From d389e7b150df6ecd215c7b661b294ea153ad0103 Mon Sep 17 00:00:00 2001
From: Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>
Date: Mon, 5 Jul 2021 13:23:03 +0200
Subject: Domain block (#76)
* start work on admin domain blocking
* move stuff around + further work on domain blocks
* move + restructure processor
* prep work for deleting account
* tidy
* go fmt
* formatting
* domain blocking more work
* check domain blocks way earlier on
* progress on delete account
* delete more stuff when an account is gone
* and more...
* domain blocky block block
* get individual domain block, delete a block
---
internal/processing/account.go | 489 +--------------------
internal/processing/account/account.go | 96 ++++
internal/processing/account/create.go | 64 +++
internal/processing/account/createfollow.go | 116 +++++
internal/processing/account/delete.go | 270 ++++++++++++
internal/processing/account/get.go | 59 +++
internal/processing/account/getfollowers.go | 79 ++++
internal/processing/account/getfollowing.go | 79 ++++
internal/processing/account/getrelationship.go | 46 ++
internal/processing/account/getstatuses.go | 63 +++
internal/processing/account/removefollow.go | 110 +++++
internal/processing/account/update.go | 199 +++++++++
internal/processing/admin.go | 56 +--
internal/processing/admin/admin.go | 60 +++
internal/processing/admin/createdomainblock.go | 154 +++++++
internal/processing/admin/deletedomainblock.go | 36 ++
internal/processing/admin/emoji.go | 73 +++
internal/processing/admin/getdomainblock.go | 30 ++
internal/processing/admin/getdomainblocks.go | 30 ++
internal/processing/federation.go | 42 +-
internal/processing/fromclientapi.go | 83 +++-
internal/processing/fromfederator.go | 326 ++------------
internal/processing/instance.go | 4 +-
internal/processing/media.go | 253 +----------
internal/processing/media/create.go | 79 ++++
internal/processing/media/delete.go | 51 +++
internal/processing/media/getfile.go | 120 +++++
internal/processing/media/getmedia.go | 51 +++
internal/processing/media/media.go | 63 +++
internal/processing/media/update.go | 70 +++
internal/processing/media/util.go | 63 +++
internal/processing/processor.go | 51 ++-
internal/processing/search.go | 6 +-
internal/processing/status/boost.go | 73 +++
internal/processing/status/boostedby.go | 68 +++
internal/processing/status/context.go | 69 +++
internal/processing/status/create.go | 106 +++++
internal/processing/status/delete.go | 56 +++
internal/processing/status/fave.go | 101 +++++
internal/processing/status/favedby.go | 68 +++
internal/processing/status/get.go | 52 +++
internal/processing/status/status.go | 57 +++
internal/processing/status/unboost.go | 95 ++++
internal/processing/status/unfave.go | 77 ++++
internal/processing/status/util.go | 269 ++++++++++++
internal/processing/streaming/authorize.go | 33 ++
internal/processing/streaming/openstream.go | 100 +++++
internal/processing/streaming/streamdelete.go | 51 +++
internal/processing/streaming/streaming.go | 52 +++
.../processing/streaming/streamnotification.go | 50 +++
internal/processing/streaming/streamstatus.go | 50 +++
internal/processing/synchronous/status/boost.go | 73 ---
.../processing/synchronous/status/boostedby.go | 68 ---
internal/processing/synchronous/status/context.go | 69 ---
internal/processing/synchronous/status/create.go | 106 -----
internal/processing/synchronous/status/delete.go | 55 ---
internal/processing/synchronous/status/fave.go | 101 -----
internal/processing/synchronous/status/favedby.go | 68 ---
internal/processing/synchronous/status/get.go | 52 ---
internal/processing/synchronous/status/status.go | 57 ---
internal/processing/synchronous/status/unboost.go | 95 ----
internal/processing/synchronous/status/unfave.go | 77 ----
internal/processing/synchronous/status/util.go | 269 ------------
.../processing/synchronous/streaming/authorize.go | 33 --
.../processing/synchronous/streaming/openstream.go | 100 -----
.../synchronous/streaming/streamdelete.go | 51 ---
.../processing/synchronous/streaming/streaming.go | 52 ---
.../synchronous/streaming/streamnotification.go | 50 ---
.../synchronous/streaming/streamstatus.go | 50 ---
internal/processing/util.go | 135 ------
70 files changed, 3682 insertions(+), 2677 deletions(-)
create mode 100644 internal/processing/account/account.go
create mode 100644 internal/processing/account/create.go
create mode 100644 internal/processing/account/createfollow.go
create mode 100644 internal/processing/account/delete.go
create mode 100644 internal/processing/account/get.go
create mode 100644 internal/processing/account/getfollowers.go
create mode 100644 internal/processing/account/getfollowing.go
create mode 100644 internal/processing/account/getrelationship.go
create mode 100644 internal/processing/account/getstatuses.go
create mode 100644 internal/processing/account/removefollow.go
create mode 100644 internal/processing/account/update.go
create mode 100644 internal/processing/admin/admin.go
create mode 100644 internal/processing/admin/createdomainblock.go
create mode 100644 internal/processing/admin/deletedomainblock.go
create mode 100644 internal/processing/admin/emoji.go
create mode 100644 internal/processing/admin/getdomainblock.go
create mode 100644 internal/processing/admin/getdomainblocks.go
create mode 100644 internal/processing/media/create.go
create mode 100644 internal/processing/media/delete.go
create mode 100644 internal/processing/media/getfile.go
create mode 100644 internal/processing/media/getmedia.go
create mode 100644 internal/processing/media/media.go
create mode 100644 internal/processing/media/update.go
create mode 100644 internal/processing/media/util.go
create mode 100644 internal/processing/status/boost.go
create mode 100644 internal/processing/status/boostedby.go
create mode 100644 internal/processing/status/context.go
create mode 100644 internal/processing/status/create.go
create mode 100644 internal/processing/status/delete.go
create mode 100644 internal/processing/status/fave.go
create mode 100644 internal/processing/status/favedby.go
create mode 100644 internal/processing/status/get.go
create mode 100644 internal/processing/status/status.go
create mode 100644 internal/processing/status/unboost.go
create mode 100644 internal/processing/status/unfave.go
create mode 100644 internal/processing/status/util.go
create mode 100644 internal/processing/streaming/authorize.go
create mode 100644 internal/processing/streaming/openstream.go
create mode 100644 internal/processing/streaming/streamdelete.go
create mode 100644 internal/processing/streaming/streaming.go
create mode 100644 internal/processing/streaming/streamnotification.go
create mode 100644 internal/processing/streaming/streamstatus.go
delete mode 100644 internal/processing/synchronous/status/boost.go
delete mode 100644 internal/processing/synchronous/status/boostedby.go
delete mode 100644 internal/processing/synchronous/status/context.go
delete mode 100644 internal/processing/synchronous/status/create.go
delete mode 100644 internal/processing/synchronous/status/delete.go
delete mode 100644 internal/processing/synchronous/status/fave.go
delete mode 100644 internal/processing/synchronous/status/favedby.go
delete mode 100644 internal/processing/synchronous/status/get.go
delete mode 100644 internal/processing/synchronous/status/status.go
delete mode 100644 internal/processing/synchronous/status/unboost.go
delete mode 100644 internal/processing/synchronous/status/unfave.go
delete mode 100644 internal/processing/synchronous/status/util.go
delete mode 100644 internal/processing/synchronous/streaming/authorize.go
delete mode 100644 internal/processing/synchronous/streaming/openstream.go
delete mode 100644 internal/processing/synchronous/streaming/streamdelete.go
delete mode 100644 internal/processing/synchronous/streaming/streaming.go
delete mode 100644 internal/processing/synchronous/streaming/streamnotification.go
delete mode 100644 internal/processing/synchronous/streaming/streamstatus.go
delete mode 100644 internal/processing/util.go
(limited to 'internal/processing')
diff --git a/internal/processing/account.go b/internal/processing/account.go
index 0e7dbbad3..ec58846d1 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -19,512 +19,43 @@
package processing
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"
- "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/util"
)
-// accountCreate does the dirty work of making an account and user in the database.
-// It then returns a token to the caller, for use with the new account, as per the
-// spec here: https://docs.joinmastodon.org/methods/accounts/
func (p *processor) AccountCreate(authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
- l := p.log.WithField("func", "accountCreate")
-
- if err := p.db.IsEmailAvailable(form.Email); err != nil {
- return nil, err
- }
-
- if err := p.db.IsUsernameAvailable(form.Username); err != nil {
- return nil, err
- }
-
- // don't store a reason if we don't require one
- reason := form.Reason
- if !p.config.AccountsConfig.ReasonRequired {
- reason = ""
- }
-
- l.Trace("creating new username and account")
- user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, authed.Application.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new signup in the database: %s", err)
- }
-
- l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, authed.Application.ID)
- accessToken, err := p.oauthServer.GenerateUserAccessToken(authed.Token, authed.Application.ClientSecret, user.ID)
- if err != nil {
- return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
- }
-
- return &apimodel.Token{
- AccessToken: accessToken.GetAccess(),
- TokenType: "Bearer",
- Scope: accessToken.GetScope(),
- CreatedAt: accessToken.GetAccessCreateAt().Unix(),
- }, nil
+ return p.accountProcessor.Create(authed.Token, authed.Application, form)
}
func (p *processor) AccountGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Account, error) {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, errors.New("account not found")
- }
- return nil, fmt.Errorf("db error: %s", err)
- }
-
- // lazily dereference things on the account if it hasn't been done yet
- var requestingUsername string
- if authed.Account != nil {
- requestingUsername = authed.Account.Username
- }
- if err := p.dereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
- p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
- }
-
- var mastoAccount *apimodel.Account
- var err error
- if authed.Account != nil && targetAccount.ID == authed.Account.ID {
- mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
- } else {
- mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
- }
- if err != nil {
- return nil, fmt.Errorf("error converting account: %s", err)
- }
- return mastoAccount, nil
+ return p.accountProcessor.Get(authed.Account, targetAccountID)
}
func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
- l := p.log.WithField("func", "AccountUpdate")
-
- if form.Discoverable != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
- return nil, fmt.Errorf("error updating discoverable: %s", err)
- }
- }
-
- if form.Bot != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
- return nil, fmt.Errorf("error updating bot: %s", err)
- }
- }
-
- if form.DisplayName != nil {
- if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Note != nil {
- if err := util.ValidateNote(*form.Note); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Avatar != nil && form.Avatar.Size != 0 {
- avatarInfo, err := p.updateAccountAvatar(form.Avatar, authed.Account.ID)
- if err != nil {
- return nil, err
- }
- l.Tracef("new avatar info for account %s is %+v", authed.Account.ID, avatarInfo)
- }
-
- if form.Header != nil && form.Header.Size != 0 {
- headerInfo, err := p.updateAccountHeader(form.Header, authed.Account.ID)
- if err != nil {
- return nil, err
- }
- l.Tracef("new header info for account %s is %+v", authed.Account.ID, headerInfo)
- }
-
- if form.Locked != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source != nil {
- if form.Source.Language != nil {
- if err := util.ValidateLanguage(*form.Source.Language); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source.Sensitive != nil {
- if err := p.db.UpdateOneByID(authed.Account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
-
- if form.Source.Privacy != nil {
- if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
- return nil, err
- }
- if err := p.db.UpdateOneByID(authed.Account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
- return nil, err
- }
- }
- }
-
- // fetch the account with all updated values set
- updatedAccount := >smodel.Account{}
- if err := p.db.GetByID(authed.Account.ID, updatedAccount); err != nil {
- return nil, fmt.Errorf("could not fetch updated account %s: %s", authed.Account.ID, err)
- }
-
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsProfile,
- APActivityType: gtsmodel.ActivityStreamsUpdate,
- GTSModel: updatedAccount,
- OriginAccount: updatedAccount,
- }
-
- acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
- if err != nil {
- return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
- }
- return acctSensitive, nil
+ 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) {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
- }
- 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 {
- if _, ok := err.(db.ErrNoEntries); ok {
- return apiStatuses, nil
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, s := range statuses {
- visible, err := p.filter.StatusVisible(&s, authed.Account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
- }
- if !visible {
- continue
- }
-
- apiStatus, err := p.tc.StatusToMasto(&s, authed.Account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
- }
-
- apiStatuses = append(apiStatuses, *apiStatus)
- }
-
- return apiStatuses, nil
+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) {
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
- }
-
- followers := []gtsmodel.Follow{}
- accounts := []apimodel.Account{}
- if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return accounts, nil
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, f := range followers {
- blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if blocked {
- continue
- }
-
- a := >smodel.Account{}
- if err := p.db.GetByID(f.AccountID, a); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- continue
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // derefence account fields in case we haven't done it already
- if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
- // don't bail if we can't fetch them, we'll try another time
- p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
- }
-
- account, err := p.tc.AccountToMastoPublic(a)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- accounts = append(accounts, *account)
- }
- return accounts, nil
+ return p.accountProcessor.FollowersGet(authed.Account, targetAccountID)
}
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if blocked {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
- }
-
- following := []gtsmodel.Follow{}
- accounts := []apimodel.Account{}
- if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return accounts, nil
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, f := range following {
- blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if blocked {
- continue
- }
-
- a := >smodel.Account{}
- if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- continue
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // derefence account fields in case we haven't done it already
- if err := p.dereferenceAccountFields(a, authed.Account.Username, false); err != nil {
- // don't bail if we can't fetch them, we'll try another time
- p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
- }
-
- account, err := p.tc.AccountToMastoPublic(a)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- accounts = append(accounts, *account)
- }
- return accounts, nil
+ return p.accountProcessor.FollowingGet(authed.Account, targetAccountID)
}
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
- if authed == nil || authed.Account == nil {
- return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
- }
-
- gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
- }
-
- r, err := p.tc.RelationshipToMasto(gtsR)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
- }
-
- return r, nil
+ return p.accountProcessor.RelationshipGet(authed.Account, targetAccountID)
}
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
- // if there's a block between the accounts we shouldn't create the request ofc
- blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if blocked {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
- }
-
- // make sure the target account actually exists in our db
- targetAcct := >smodel.Account{}
- if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
- }
- }
-
- // check if a follow exists already
- follows, err := p.db.Follows(authed.Account, targetAcct)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
- }
- if follows {
- // already follows so just return the relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // check if a follow exists already
- followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
- }
- if followRequested {
- // already follow requested so just return the relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // make the follow request
- newFollowID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- fr := >smodel.FollowRequest{
- ID: newFollowID,
- AccountID: authed.Account.ID,
- TargetAccountID: form.TargetAccountID,
- ShowReblogs: true,
- URI: util.GenerateURIForFollow(authed.Account.Username, p.config.Protocol, p.config.Host, newFollowID),
- Notify: false,
- }
- if form.Reblogs != nil {
- fr.ShowReblogs = *form.Reblogs
- }
- if form.Notify != nil {
- fr.Notify = *form.Notify
- }
-
- // whack it in the database
- if err := p.db.Put(fr); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
- }
-
- // if it's a local account that's not locked we can just straight up accept the follow request
- if !targetAcct.Locked && targetAcct.Domain == "" {
- if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
- }
- // return the new relationship
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
- }
-
- // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: fr,
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
-
- // return whatever relationship results from this
- return p.AccountRelationshipGet(authed, form.TargetAccountID)
+ return p.accountProcessor.FollowCreate(authed.Account, form)
}
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
- // if there's a block between the accounts we shouldn't do anything
- blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- if blocked {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
- }
-
- // make sure the target account actually exists in our db
- targetAcct := >smodel.Account{}
- if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
- }
- }
-
- // check if a follow request exists, and remove it if it does (storing the URI for later)
- var frChanged bool
- var frURI string
- fr := >smodel.FollowRequest{}
- if err := p.db.GetWhere([]db.Where{
- {Key: "account_id", Value: authed.Account.ID},
- {Key: "target_account_id", Value: targetAccountID},
- }, fr); err == nil {
- frURI = fr.URI
- if err := p.db.DeleteByID(fr.ID, fr); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
- }
- frChanged = true
- }
-
- // now do the same thing for any existing follow
- var fChanged bool
- var fURI string
- f := >smodel.Follow{}
- if err := p.db.GetWhere([]db.Where{
- {Key: "account_id", Value: authed.Account.ID},
- {Key: "target_account_id", Value: targetAccountID},
- }, f); err == nil {
- fURI = f.URI
- if err := p.db.DeleteByID(f.ID, f); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
- }
- fChanged = true
- }
-
- // follow request status changed so send the UNDO activity to the channel for async processing
- if frChanged {
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: >smodel.Follow{
- AccountID: authed.Account.ID,
- TargetAccountID: targetAccountID,
- URI: frURI,
- },
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
- }
-
- // follow status changed so send the UNDO activity to the channel for async processing
- if fChanged {
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsFollow,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: >smodel.Follow{
- AccountID: authed.Account.ID,
- TargetAccountID: targetAccountID,
- URI: fURI,
- },
- OriginAccount: authed.Account,
- TargetAccount: targetAcct,
- }
- }
-
- // return whatever relationship results from all this
- return p.AccountRelationshipGet(authed, targetAccountID)
+ return p.accountProcessor.FollowRemove(authed.Account, targetAccountID)
}
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
new file mode 100644
index 000000000..442b6fa9f
--- /dev/null
+++ b/internal/processing/account/account.go
@@ -0,0 +1,96 @@
+/*
+ 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 account
+
+import (
+ "mime/multipart"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "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/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+ "github.com/superseriousbusiness/oauth2/v4"
+)
+
+// Processor wraps a bunch of functions for processing account actions.
+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, 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
+ Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
+ // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
+ // the account given in authed.
+ StatusesGet(requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
+ // FollowersGet fetches a list of the target account's followers.
+ FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
+ // FollowingGet fetches a list of the accounts that target account is following.
+ FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
+ // RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
+ RelationshipGet(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
+ // FollowCreate handles a follow request to an account, either remote or local.
+ FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
+ // FollowRemove handles the removal of a follow/follow request to an account, either remote or local.
+ FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
+ // UpdateHeader does the dirty work of checking the header part of an account update form,
+ // parsing and checking the image, and doing the necessary updates in the database for this to become
+ // the account's new header image.
+ UpdateAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
+ // UpdateAvatar does the dirty work of checking the avatar part of an account update form,
+ // parsing and checking the image, and doing the necessary updates in the database for this to become
+ // the account's new avatar image.
+ UpdateHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error)
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ mediaHandler media.Handler
+ fromClientAPI chan gtsmodel.FromClientAPI
+ oauthServer oauth.Server
+ filter visibility.Filter
+ db db.DB
+ federator federation.Federator
+ log *logrus.Logger
+}
+
+// New returns a new account processor.
+func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, oauthServer oauth.Server, fromClientAPI chan gtsmodel.FromClientAPI, federator federation.Federator, config *config.Config, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ mediaHandler: mediaHandler,
+ fromClientAPI: fromClientAPI,
+ oauthServer: oauthServer,
+ filter: visibility.NewFilter(db, log),
+ db: db,
+ federator: federator,
+ log: log,
+ }
+}
diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go
new file mode 100644
index 000000000..a6bfb8a60
--- /dev/null
+++ b/internal/processing/account/create.go
@@ -0,0 +1,64 @@
+/*
+ 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 account
+
+import (
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/oauth2/v4"
+)
+
+func (p *processor) Create(applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, error) {
+ l := p.log.WithField("func", "accountCreate")
+
+ if err := p.db.IsEmailAvailable(form.Email); err != nil {
+ return nil, err
+ }
+
+ if err := p.db.IsUsernameAvailable(form.Username); err != nil {
+ return nil, err
+ }
+
+ // don't store a reason if we don't require one
+ reason := form.Reason
+ if !p.config.AccountsConfig.ReasonRequired {
+ reason = ""
+ }
+
+ l.Trace("creating new username and account")
+ user, err := p.db.NewSignup(form.Username, reason, p.config.AccountsConfig.RequireApproval, form.Email, form.Password, form.IP, form.Locale, application.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error creating new signup in the database: %s", err)
+ }
+
+ l.Tracef("generating a token for user %s with account %s and application %s", user.ID, user.AccountID, application.ID)
+ accessToken, err := p.oauthServer.GenerateUserAccessToken(applicationToken, application.ClientSecret, user.ID)
+ if err != nil {
+ return nil, fmt.Errorf("error creating new access token for user %s: %s", user.ID, err)
+ }
+
+ return &apimodel.Token{
+ AccessToken: accessToken.GetAccess(),
+ TokenType: "Bearer",
+ Scope: accessToken.GetScope(),
+ CreatedAt: accessToken.GetAccessCreateAt().Unix(),
+ }, nil
+}
diff --git a/internal/processing/account/createfollow.go b/internal/processing/account/createfollow.go
new file mode 100644
index 000000000..50e575f19
--- /dev/null
+++ b/internal/processing/account/createfollow.go
@@ -0,0 +1,116 @@
+/*
+ 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 account
+
+import (
+ "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"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) FollowCreate(requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
+ // if there's a block between the accounts we shouldn't create the request ofc
+ blocked, err := p.db.Blocked(requestingAccount.ID, form.TargetAccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if blocked {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
+ }
+
+ // make sure the target account actually exists in our db
+ targetAcct := >smodel.Account{}
+ if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
+ }
+ }
+
+ // check if a follow exists already
+ follows, err := p.db.Follows(requestingAccount, targetAcct)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
+ }
+ if follows {
+ // already follows so just return the relationship
+ return p.RelationshipGet(requestingAccount, form.TargetAccountID)
+ }
+
+ // check if a follow exists already
+ followRequested, err := p.db.FollowRequested(requestingAccount, targetAcct)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
+ }
+ if followRequested {
+ // already follow requested so just return the relationship
+ return p.RelationshipGet(requestingAccount, form.TargetAccountID)
+ }
+
+ // make the follow request
+ newFollowID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ fr := >smodel.FollowRequest{
+ ID: newFollowID,
+ AccountID: requestingAccount.ID,
+ TargetAccountID: form.TargetAccountID,
+ ShowReblogs: true,
+ URI: util.GenerateURIForFollow(requestingAccount.Username, p.config.Protocol, p.config.Host, newFollowID),
+ Notify: false,
+ }
+ if form.Reblogs != nil {
+ fr.ShowReblogs = *form.Reblogs
+ }
+ if form.Notify != nil {
+ fr.Notify = *form.Notify
+ }
+
+ // whack it in the database
+ if err := p.db.Put(fr); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
+ }
+
+ // if it's a local account that's not locked we can just straight up accept the follow request
+ if !targetAcct.Locked && targetAcct.Domain == "" {
+ if _, err := p.db.AcceptFollowRequest(requestingAccount.ID, form.TargetAccountID); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
+ }
+ // return the new relationship
+ return p.RelationshipGet(requestingAccount, form.TargetAccountID)
+ }
+
+ // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: fr,
+ OriginAccount: requestingAccount,
+ TargetAccount: targetAcct,
+ }
+
+ // return whatever relationship results from this
+ return p.RelationshipGet(requestingAccount, form.TargetAccountID)
+}
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
new file mode 100644
index 000000000..c31b67352
--- /dev/null
+++ b/internal/processing/account/delete.go
@@ -0,0 +1,270 @@
+/*
+ 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 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"
+)
+
+// Delete handles the complete deletion of an account.
+//
+// TODO in this function:
+// 1. Delete account's application(s), clients, and oauth tokens
+// 2. Delete account's blocks
+// 3. Delete account's emoji
+// 4. Delete account's follow requests
+// 5. Delete account's follows
+// 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 notifications
+// 11. Delete account's bookmarks
+// 12. Delete account's faves
+// 13. Delete account's mutes
+// 14. Delete account's streams
+// 15. Delete account's tags
+// 16. Delete account's user
+// 17. Delete account's timeline
+// 18. Delete account itself
+func (p *processor) Delete(account *gtsmodel.Account, deletedBy string) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "Delete",
+ "username": account.Username,
+ })
+
+ l.Debugf("beginning account delete process for username %s", account.Username)
+
+ // 1. Delete account's application(s), clients, and oauth tokens
+ // we only need to do this step for local account since remote ones won't have any tokens or applications on our server
+ if account.Domain == "" {
+ // see if we can get a user for this account
+ u := >smodel.User{}
+ if err := p.db.GetWhere([]db.Where{{Key: "account_id", Value: account.ID}}, u); err == nil {
+ // we got one! select all tokens with the user's ID
+ tokens := []*oauth.Token{}
+ if err := p.db.GetWhere([]db.Where{{Key: "user_id", Value: u.ID}}, &tokens); err == nil {
+ // we have some tokens to delete
+ for _, t := range tokens {
+ // delete client(s) associated with this token
+ if err := p.db.DeleteByID(t.ClientID, &oauth.Client{}); err != nil {
+ l.Errorf("error deleting oauth client: %s", err)
+ }
+ // delete application(s) associated with this token
+ if err := p.db.DeleteWhere([]db.Where{{Key: "client_id", Value: t.ClientID}}, >smodel.Application{}); err != nil {
+ l.Errorf("error deleting application: %s", err)
+ }
+ // delete the token itself
+ if err := p.db.DeleteByID(t.ID, t); err != nil {
+ l.Errorf("error deleting oauth token: %s", err)
+ }
+ }
+ }
+ }
+ }
+
+ // 2. Delete account's blocks
+ l.Debug("deleting account blocks")
+ // first delete any blocks that this account created
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil {
+ l.Errorf("error deleting blocks created by account: %s", err)
+ }
+
+ // now delete any blocks that target this account
+ if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Block{}); err != nil {
+ l.Errorf("error deleting blocks targeting account: %s", err)
+ }
+
+ // 3. Delete account's emoji
+ // nothing to do here
+
+ // 4. Delete account's follow requests
+ l.Debug("deleting account follow requests")
+ // first delete any follow requests that this account created
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
+ l.Errorf("error deleting follow requests created by account: %s", err)
+ }
+
+ // now delete any follow requests that target this account
+ if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.FollowRequest{}); err != nil {
+ l.Errorf("error deleting follow requests targeting account: %s", err)
+ }
+
+ // 5. Delete account's follows
+ l.Debug("deleting account follows")
+ // first delete any follows that this account created
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
+ l.Errorf("error deleting follows created by account: %s", err)
+ }
+
+ // now delete any follows that target this account
+ if err := p.db.DeleteWhere([]db.Where{{Key: "target_account_id", Value: account.ID}}, &[]*gtsmodel.Follow{}); err != nil {
+ l.Errorf("error deleting follows targeting account: %s", err)
+ }
+
+ // 6. Delete account's statuses
+ l.Debug("deleting account 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 statuses 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
+ l.Debug("putting status in the client api channel")
+ 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 there are any boosts of this status, delete them as well
+ boosts := []*gtsmodel.Status{}
+ if err := p.db.GetWhere([]db.Where{{Key: "boost_of_id", Value: s.ID}}, &boosts); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // an actual error has occurred
+ l.Errorf("Delete: db error selecting boosts of status %s for account %s: %s", s.ID, account.Username, err)
+ break selectStatusesLoop
+ }
+ }
+
+ for _, b := range boosts {
+ oa := >smodel.Account{}
+ if err := p.db.GetByID(b.AccountID, oa); err == nil {
+
+ l.Debug("putting boost undo in the client api channel")
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: s,
+ OriginAccount: oa,
+ TargetAccount: account,
+ }
+ }
+
+ if err := p.db.DeleteByID(b.ID, b); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // actual error has occurred
+ l.Errorf("Delete: db error deleting boost with id %s: %s", b.ID, 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
+ }
+ }
+ }
+ l.Debug("done deleting statuses")
+
+ // 10. Delete account's notifications
+ l.Debug("deleting account 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)
+ }
+
+ // 11. Delete account's bookmarks
+ l.Debug("deleting account bookmarks")
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusBookmark{}); err != nil {
+ l.Errorf("error deleting bookmarks created by account: %s", err)
+ }
+
+ // 12. Delete account's faves
+ l.Debug("deleting account faves")
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusFave{}); err != nil {
+ l.Errorf("error deleting faves created by account: %s", err)
+ }
+
+ // 13. Delete account's mutes
+ l.Debug("deleting account mutes")
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, &[]*gtsmodel.StatusMute{}); err != nil {
+ l.Errorf("error deleting status mutes created by account: %s", err)
+ }
+
+ // 14. Delete account's streams
+
+ // 15. Delete account's tags
+ // TODO
+
+ // 16. Delete account's user
+ l.Debug("deleting account user")
+ if err := p.db.DeleteWhere([]db.Where{{Key: "account_id", Value: account.ID}}, >smodel.User{}); err != nil {
+ return err
+ }
+
+ // 17. Delete account's timeline
+
+ // 18. Delete account itself
+ // 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
+
+ if err := p.db.UpdateByID(account.ID, account); err != nil {
+ return err
+ }
+
+ l.Infof("deleted account with username %s from domain %s", account.Username, account.Domain)
+ return nil
+}
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
new file mode 100644
index 000000000..aba1ed14a
--- /dev/null
+++ b/internal/processing/account/get.go
@@ -0,0 +1,59 @@
+/*
+ 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 account
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Get(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, error) {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, errors.New("account not found")
+ }
+ return nil, fmt.Errorf("db error: %s", err)
+ }
+
+ // lazily dereference things on the account if it hasn't been done yet
+ var requestingUsername string
+ if requestingAccount != nil {
+ requestingUsername = requestingAccount.Username
+ }
+ if err := p.federator.DereferenceAccountFields(targetAccount, requestingUsername, false); err != nil {
+ p.log.WithField("func", "AccountGet").Debugf("dereferencing account: %s", err)
+ }
+
+ var mastoAccount *apimodel.Account
+ var err error
+ if requestingAccount != nil && targetAccount.ID == requestingAccount.ID {
+ mastoAccount, err = p.tc.AccountToMastoSensitive(targetAccount)
+ } else {
+ mastoAccount, err = p.tc.AccountToMastoPublic(targetAccount)
+ }
+ if err != nil {
+ return nil, fmt.Errorf("error converting account: %s", err)
+ }
+ return mastoAccount, nil
+}
diff --git a/internal/processing/account/getfollowers.go b/internal/processing/account/getfollowers.go
new file mode 100644
index 000000000..bfc463d3f
--- /dev/null
+++ b/internal/processing/account/getfollowers.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 account
+
+import (
+ "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) FollowersGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
+ blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
+ }
+
+ followers := []gtsmodel.Follow{}
+ accounts := []apimodel.Account{}
+ if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return accounts, nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, f := range followers {
+ blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if blocked {
+ continue
+ }
+
+ a := >smodel.Account{}
+ if err := p.db.GetByID(f.AccountID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ continue
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // derefence account fields in case we haven't done it already
+ if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
+ // don't bail if we can't fetch them, we'll try another time
+ p.log.WithField("func", "AccountFollowersGet").Debugf("error dereferencing account fields: %s", err)
+ }
+
+ account, err := p.tc.AccountToMastoPublic(a)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ accounts = append(accounts, *account)
+ }
+ return accounts, nil
+}
diff --git a/internal/processing/account/getfollowing.go b/internal/processing/account/getfollowing.go
new file mode 100644
index 000000000..bb6a905f4
--- /dev/null
+++ b/internal/processing/account/getfollowing.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 account
+
+import (
+ "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) FollowingGet(requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
+ blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
+ }
+
+ following := []gtsmodel.Follow{}
+ accounts := []apimodel.Account{}
+ if err := p.db.GetFollowingByAccountID(targetAccountID, &following); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return accounts, nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, f := range following {
+ blocked, err := p.db.Blocked(requestingAccount.ID, f.AccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if blocked {
+ continue
+ }
+
+ a := >smodel.Account{}
+ if err := p.db.GetByID(f.TargetAccountID, a); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ continue
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // derefence account fields in case we haven't done it already
+ if err := p.federator.DereferenceAccountFields(a, requestingAccount.Username, false); err != nil {
+ // don't bail if we can't fetch them, we'll try another time
+ p.log.WithField("func", "AccountFollowingGet").Debugf("error dereferencing account fields: %s", err)
+ }
+
+ account, err := p.tc.AccountToMastoPublic(a)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ accounts = append(accounts, *account)
+ }
+ return accounts, nil
+}
diff --git a/internal/processing/account/getrelationship.go b/internal/processing/account/getrelationship.go
new file mode 100644
index 000000000..a0a93a4c2
--- /dev/null
+++ b/internal/processing/account/getrelationship.go
@@ -0,0 +1,46 @@
+/*
+ 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 account
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) RelationshipGet(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
+ if requestingAccount == nil {
+ return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
+ }
+
+ gtsR, err := p.db.GetRelationship(requestingAccount.ID, targetAccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
+ }
+
+ r, err := p.tc.RelationshipToMasto(gtsR)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
+ }
+
+ return r, nil
+}
diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go
new file mode 100644
index 000000000..b8ccbc528
--- /dev/null
+++ b/internal/processing/account/getstatuses.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 account
+
+import (
+ "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) 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 {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ apiStatuses := []apimodel.Status{}
+ statuses, err := p.db.GetStatusesForAccount(targetAccountID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return apiStatuses, nil
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, s := range statuses {
+ visible, err := p.filter.StatusVisible(s, requestingAccount)
+ if err != nil || !visible {
+ continue
+ }
+
+ apiStatus, err := p.tc.StatusToMasto(s, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
+ }
+
+ apiStatuses = append(apiStatuses, *apiStatus)
+ }
+
+ return apiStatuses, nil
+}
diff --git a/internal/processing/account/removefollow.go b/internal/processing/account/removefollow.go
new file mode 100644
index 000000000..ef8994893
--- /dev/null
+++ b/internal/processing/account/removefollow.go
@@ -0,0 +1,110 @@
+/*
+ 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 account
+
+import (
+ "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) FollowRemove(requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
+ // if there's a block between the accounts we shouldn't do anything
+ blocked, err := p.db.Blocked(requestingAccount.ID, targetAccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if blocked {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
+ }
+
+ // make sure the target account actually exists in our db
+ targetAcct := >smodel.Account{}
+ if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
+ }
+ }
+
+ // check if a follow request exists, and remove it if it does (storing the URI for later)
+ var frChanged bool
+ var frURI string
+ fr := >smodel.FollowRequest{}
+ if err := p.db.GetWhere([]db.Where{
+ {Key: "account_id", Value: requestingAccount.ID},
+ {Key: "target_account_id", Value: targetAccountID},
+ }, fr); err == nil {
+ frURI = fr.URI
+ if err := p.db.DeleteByID(fr.ID, fr); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
+ }
+ frChanged = true
+ }
+
+ // now do the same thing for any existing follow
+ var fChanged bool
+ var fURI string
+ f := >smodel.Follow{}
+ if err := p.db.GetWhere([]db.Where{
+ {Key: "account_id", Value: requestingAccount.ID},
+ {Key: "target_account_id", Value: targetAccountID},
+ }, f); err == nil {
+ fURI = f.URI
+ if err := p.db.DeleteByID(f.ID, f); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
+ }
+ fChanged = true
+ }
+
+ // follow request status changed so send the UNDO activity to the channel for async processing
+ if frChanged {
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: >smodel.Follow{
+ AccountID: requestingAccount.ID,
+ TargetAccountID: targetAccountID,
+ URI: frURI,
+ },
+ OriginAccount: requestingAccount,
+ TargetAccount: targetAcct,
+ }
+ }
+
+ // follow status changed so send the UNDO activity to the channel for async processing
+ if fChanged {
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsFollow,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: >smodel.Follow{
+ AccountID: requestingAccount.ID,
+ TargetAccountID: targetAccountID,
+ URI: fURI,
+ },
+ OriginAccount: requestingAccount,
+ TargetAccount: targetAcct,
+ }
+ }
+
+ // return whatever relationship results from all this
+ return p.RelationshipGet(requestingAccount, targetAccountID)
+}
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
new file mode 100644
index 000000000..830fec60a
--- /dev/null
+++ b/internal/processing/account/update.go
@@ -0,0 +1,199 @@
+/*
+ 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 account
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Update(account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) {
+ l := p.log.WithField("func", "AccountUpdate")
+
+ if form.Discoverable != nil {
+ if err := p.db.UpdateOneByID(account.ID, "discoverable", *form.Discoverable, >smodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating discoverable: %s", err)
+ }
+ }
+
+ if form.Bot != nil {
+ if err := p.db.UpdateOneByID(account.ID, "bot", *form.Bot, >smodel.Account{}); err != nil {
+ return nil, fmt.Errorf("error updating bot: %s", err)
+ }
+ }
+
+ if form.DisplayName != nil {
+ if err := util.ValidateDisplayName(*form.DisplayName); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(account.ID, "display_name", *form.DisplayName, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Note != nil {
+ if err := util.ValidateNote(*form.Note); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(account.ID, "note", *form.Note, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Avatar != nil && form.Avatar.Size != 0 {
+ avatarInfo, err := p.UpdateAvatar(form.Avatar, account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new avatar info for account %s is %+v", account.ID, avatarInfo)
+ }
+
+ if form.Header != nil && form.Header.Size != 0 {
+ headerInfo, err := p.UpdateHeader(form.Header, account.ID)
+ if err != nil {
+ return nil, err
+ }
+ l.Tracef("new header info for account %s is %+v", account.ID, headerInfo)
+ }
+
+ if form.Locked != nil {
+ if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source != nil {
+ if form.Source.Language != nil {
+ if err := util.ValidateLanguage(*form.Source.Language); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(account.ID, "language", *form.Source.Language, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Sensitive != nil {
+ if err := p.db.UpdateOneByID(account.ID, "locked", *form.Locked, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+
+ if form.Source.Privacy != nil {
+ if err := util.ValidatePrivacy(*form.Source.Privacy); err != nil {
+ return nil, err
+ }
+ if err := p.db.UpdateOneByID(account.ID, "privacy", *form.Source.Privacy, >smodel.Account{}); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ // fetch the account with all updated values set
+ updatedAccount := >smodel.Account{}
+ if err := p.db.GetByID(account.ID, updatedAccount); err != nil {
+ return nil, fmt.Errorf("could not fetch updated account %s: %s", account.ID, err)
+ }
+
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsUpdate,
+ GTSModel: updatedAccount,
+ OriginAccount: updatedAccount,
+ }
+
+ acctSensitive, err := p.tc.AccountToMastoSensitive(updatedAccount)
+ if err != nil {
+ return nil, fmt.Errorf("could not convert account into mastosensitive account: %s", err)
+ }
+ return acctSensitive, nil
+}
+
+// UpdateAvatar does the dirty work of checking the avatar part of an account update form,
+// parsing and checking the image, and doing the necessary updates in the database for this to become
+// the account's new avatar image.
+func (p *processor) UpdateAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
+ return nil, err
+ }
+ f, err := avatar.Open()
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided avatar: %s", err)
+ }
+
+ // extract the bytes
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided avatar: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided avatar: size 0 bytes")
+ }
+
+ // do the setting
+ avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
+ if err != nil {
+ return nil, fmt.Errorf("error processing avatar: %s", err)
+ }
+
+ return avatarInfo, f.Close()
+}
+
+// UpdateHeader does the dirty work of checking the header part of an account update form,
+// parsing and checking the image, and doing the necessary updates in the database for this to become
+// the account's new header image.
+func (p *processor) UpdateHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
+ var err error
+ if int(header.Size) > p.config.MediaConfig.MaxImageSize {
+ err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
+ return nil, err
+ }
+ f, err := header.Open()
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided header: %s", err)
+ }
+
+ // extract the bytes
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("could not read provided header: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided header: size 0 bytes")
+ }
+
+ // do the setting
+ headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
+ if err != nil {
+ return nil, fmt.Errorf("error processing header: %s", err)
+ }
+
+ return headerInfo, f.Close()
+}
diff --git a/internal/processing/admin.go b/internal/processing/admin.go
index 6ee3a059f..73ac32c7a 100644
--- a/internal/processing/admin.go
+++ b/internal/processing/admin.go
@@ -19,55 +19,27 @@
package processing
import (
- "bytes"
- "errors"
- "fmt"
- "io"
-
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
- if !authed.User.Admin {
- return nil, fmt.Errorf("user %s not an admin", authed.User.ID)
- }
-
- // open the emoji and extract the bytes from it
- f, err := form.Image.Open()
- if err != nil {
- return nil, fmt.Errorf("error opening emoji: %s", err)
- }
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("error reading emoji: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided emoji: size 0 bytes")
- }
-
- // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
- emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
- if err != nil {
- return nil, fmt.Errorf("error reading emoji: %s", err)
- }
+ return p.adminProcessor.EmojiCreate(authed.Account, authed.User, form)
+}
- emojiID, err := id.NewULID()
- if err != nil {
- return nil, err
- }
- emoji.ID = emojiID
+func (p *processor) AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
+ return p.adminProcessor.DomainBlockCreate(authed.Account, form)
+}
- mastoEmoji, err := p.tc.EmojiToMasto(emoji)
- if err != nil {
- return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
- }
+func (p *processor) AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
+ return p.adminProcessor.DomainBlocksGet(authed.Account, export)
+}
- if err := p.db.Put(emoji); err != nil {
- return nil, fmt.Errorf("database error while processing emoji: %s", err)
- }
+func (p *processor) AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
+ return p.adminProcessor.DomainBlockGet(authed.Account, id, export)
+}
- return &mastoEmoji, nil
+func (p *processor) AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
+ return p.adminProcessor.DomainBlockDelete(authed.Account, id)
}
diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go
new file mode 100644
index 000000000..8cb1d7f78
--- /dev/null
+++ b/internal/processing/admin/admin.go
@@ -0,0 +1,60 @@
+/*
+ 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 admin
+
+import (
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "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 admin actions.
+type Processor interface {
+ DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
+ DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
+ DomainBlockGet(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
+ DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode)
+ EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ mediaHandler media.Handler
+ fromClientAPI chan gtsmodel.FromClientAPI
+ db db.DB
+ log *logrus.Logger
+}
+
+// New returns a new admin processor.
+func New(db db.DB, tc typeutils.TypeConverter, mediaHandler media.Handler, fromClientAPI chan gtsmodel.FromClientAPI, config *config.Config, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ mediaHandler: mediaHandler,
+ fromClientAPI: fromClientAPI,
+ db: db,
+ log: log,
+ }
+}
diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go
new file mode 100644
index 000000000..82a7cac4f
--- /dev/null
+++ b/internal/processing/admin/createdomainblock.go
@@ -0,0 +1,154 @@
+/*
+ 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 admin
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/sirupsen/logrus"
+ 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/id"
+)
+
+func (p *processor) DomainBlockCreate(account *gtsmodel.Account, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) {
+ // first check if we already have a block -- if err == nil we already had a block so we can skip a whole lot of work
+ domainBlock := >smodel.DomainBlock{}
+ err := p.db.GetWhere([]db.Where{{Key: "domain", Value: form.Domain, CaseInsensitive: true}}, domainBlock)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something went wrong in the DB
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error checking for existence of domain block %s: %s", form.Domain, err))
+ }
+
+ // there's no block for this domain yet so create one
+ // note: we take a new ulid from timestamp here in case we need to sort blocks
+ blockID, err := id.NewULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error creating id for new domain block %s: %s", form.Domain, err))
+ }
+
+ domainBlock = >smodel.DomainBlock{
+ ID: blockID,
+ Domain: form.Domain,
+ CreatedByAccountID: account.ID,
+ PrivateComment: form.PrivateComment,
+ PublicComment: form.PublicComment,
+ Obfuscate: form.Obfuscate,
+ }
+
+ // put the new block in the database
+ if err := p.db.Put(domainBlock); err != nil {
+ if _, ok := err.(db.ErrAlreadyExists); !ok {
+ // there's a real error creating the block
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: db error putting new domain block %s: %s", form.Domain, err))
+ }
+ }
+
+ // process the side effects of the domain block asynchronously since it might take a while
+ go p.initiateDomainBlockSideEffects(account, domainBlock) // TODO: add this to a queuing system so it can retry/resume
+ }
+
+ mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("DomainBlockCreate: error converting domain block to frontend/masto representation %s: %s", form.Domain, err))
+ }
+
+ return mastoDomainBlock, nil
+}
+
+// initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block:
+//
+// 1. Strip most info away from the instance entry for the domain.
+// 2. Delete the instance account for that instance if it exists.
+// 3. Select all accounts from this instance and pass them through the delete functionality of the processor.
+func (p *processor) initiateDomainBlockSideEffects(account *gtsmodel.Account, block *gtsmodel.DomainBlock) {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "domainBlockProcessSideEffects",
+ "domain": block.Domain,
+ })
+
+ l.Debug("processing domain block side effects")
+
+ // if we have an instance entry for this domain, update it with the new block ID and clear all fields
+ instance := >smodel.Instance{}
+ if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: block.Domain, CaseInsensitive: true}}, instance); err == nil {
+ instance.Title = ""
+ instance.UpdatedAt = time.Now()
+ instance.SuspendedAt = time.Now()
+ instance.DomainBlockID = block.ID
+ instance.ShortDescription = ""
+ instance.Description = ""
+ instance.Terms = ""
+ instance.ContactEmail = ""
+ instance.ContactAccountUsername = ""
+ instance.ContactAccountID = ""
+ instance.Version = ""
+ if err := p.db.UpdateByID(instance.ID, instance); err != nil {
+ l.Errorf("domainBlockProcessSideEffects: db error updating instance: %s", err)
+ }
+ l.Debug("domainBlockProcessSideEffects: instance entry updated")
+ }
+
+ // if we have an instance account for this instance, delete it
+ if err := p.db.DeleteWhere([]db.Where{{Key: "username", Value: block.Domain, CaseInsensitive: true}}, >smodel.Account{}); err != nil {
+ l.Errorf("domainBlockProcessSideEffects: db error removing instance account: %s", err)
+ }
+
+ // delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines)
+
+ limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query
+ var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID)
+
+selectAccountsLoop:
+ for {
+ accounts, err := p.db.GetAccountsForInstance(block.Domain, maxID, limit)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ // no accounts left for this instance so we're done
+ l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain)
+ break selectAccountsLoop
+ }
+ // an actual error has occurred
+ l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err)
+ break selectAccountsLoop
+ }
+
+ for i, a := range accounts {
+ l.Debugf("putting delete for account %s in the clientAPI channel", a.Username)
+
+ // pass the account delete through the client api channel for processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsPerson,
+ APActivityType: gtsmodel.ActivityStreamsDelete,
+ GTSModel: a,
+ OriginAccount: account,
+ TargetAccount: a,
+ }
+
+ // if this is the last account in the slice, set the maxID appropriately for the next query
+ if i == len(accounts)-1 {
+ maxID = a.ID
+ }
+ }
+ }
+}
diff --git a/internal/processing/admin/deletedomainblock.go b/internal/processing/admin/deletedomainblock.go
new file mode 100644
index 000000000..973344dc2
--- /dev/null
+++ b/internal/processing/admin/deletedomainblock.go
@@ -0,0 +1,36 @@
+package admin
+
+import (
+ "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) DomainBlockDelete(account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) {
+ domainBlock := >smodel.DomainBlock{}
+
+ if err := p.db.GetByID(id, domainBlock); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something has gone really wrong
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ // there are no entries for this ID
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
+ }
+
+ // prepare the domain block to return
+ mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, false)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // delete the domain block
+ if err := p.db.DeleteByID(id, domainBlock); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return mastoDomainBlock, nil
+}
diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go
new file mode 100644
index 000000000..f19e173b5
--- /dev/null
+++ b/internal/processing/admin/emoji.go
@@ -0,0 +1,73 @@
+/*
+ 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 admin
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+)
+
+func (p *processor) EmojiCreate(account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error) {
+ if user.Admin {
+ return nil, fmt.Errorf("user %s not an admin", user.ID)
+ }
+
+ // open the emoji and extract the bytes from it
+ f, err := form.Image.Open()
+ if err != nil {
+ return nil, fmt.Errorf("error opening emoji: %s", err)
+ }
+ buf := new(bytes.Buffer)
+ size, err := io.Copy(buf, f)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+ if size == 0 {
+ return nil, errors.New("could not read provided emoji: size 0 bytes")
+ }
+
+ // allow the mediaHandler to work its magic of processing the emoji bytes, and putting them in whatever storage backend we're using
+ emoji, err := p.mediaHandler.ProcessLocalEmoji(buf.Bytes(), form.Shortcode)
+ if err != nil {
+ return nil, fmt.Errorf("error reading emoji: %s", err)
+ }
+
+ emojiID, err := id.NewULID()
+ if err != nil {
+ return nil, err
+ }
+ emoji.ID = emojiID
+
+ mastoEmoji, err := p.tc.EmojiToMasto(emoji)
+ if err != nil {
+ return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
+ }
+
+ if err := p.db.Put(emoji); err != nil {
+ return nil, fmt.Errorf("database error while processing emoji: %s", err)
+ }
+
+ return &mastoEmoji, nil
+}
diff --git a/internal/processing/admin/getdomainblock.go b/internal/processing/admin/getdomainblock.go
new file mode 100644
index 000000000..170d82e0b
--- /dev/null
+++ b/internal/processing/admin/getdomainblock.go
@@ -0,0 +1,30 @@
+package admin
+
+import (
+ "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) DomainBlockGet(account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) {
+ domainBlock := >smodel.DomainBlock{}
+
+ if err := p.db.GetByID(id, domainBlock); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something has gone really wrong
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ // there are no entries for this ID
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id))
+ }
+
+ mastoDomainBlock, err := p.tc.DomainBlockToMasto(domainBlock, export)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return mastoDomainBlock, nil
+}
diff --git a/internal/processing/admin/getdomainblocks.go b/internal/processing/admin/getdomainblocks.go
new file mode 100644
index 000000000..57e2ca7af
--- /dev/null
+++ b/internal/processing/admin/getdomainblocks.go
@@ -0,0 +1,30 @@
+package admin
+
+import (
+ 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) DomainBlocksGet(account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) {
+ domainBlocks := []*gtsmodel.DomainBlock{}
+
+ if err := p.db.GetAll(&domainBlocks); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ // something has gone really wrong
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ mastoDomainBlocks := []*apimodel.DomainBlock{}
+ for _, b := range domainBlocks {
+ mastoDomainBlock, err := p.tc.DomainBlockToMasto(b, export)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ mastoDomainBlocks = append(mastoDomainBlocks, mastoDomainBlock)
+ }
+
+ return mastoDomainBlocks, nil
+}
diff --git a/internal/processing/federation.go b/internal/processing/federation.go
index 3bcda866b..6299d5e7e 100644
--- a/internal/processing/federation.go
+++ b/internal/processing/federation.go
@@ -20,6 +20,7 @@ package processing
import (
"context"
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -89,7 +90,7 @@ func (p *processor) dereferenceFediRequest(username string, requestingAccountURI
return requestingAccount, nil
}
-func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
+func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@@ -98,17 +99,17 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
var requestedPerson vocab.ActivityStreamsPerson
var err error
- if util.IsPublicKeyPath(request.URL) {
+ if util.IsPublicKeyPath(requestURL) {
// if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key
requestedPerson, err = p.tc.AccountToASMinimal(requestedAccount)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- } else if util.IsUserPath(request.URL) {
+ } else if util.IsUserPath(requestURL) {
// if it's a user path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile
- requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
// if we're already handshaking/dereferencing a remote account, we can skip the dereferencing part
@@ -144,7 +145,7 @@ func (p *processor) GetFediUser(requestedUsername string, request *http.Request)
return data, nil
}
-func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
+func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@@ -152,9 +153,9 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
}
// authenticate the request
- requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@@ -189,7 +190,7 @@ func (p *processor) GetFediFollowers(requestedUsername string, request *http.Req
return data, nil
}
-func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
+func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@@ -197,9 +198,9 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
}
// authenticate the request
- requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@@ -234,7 +235,7 @@ func (p *processor) GetFediFollowing(requestedUsername string, request *http.Req
return data, nil
}
-func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) {
+func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@@ -242,9 +243,9 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
}
// authenticate the request
- requestingAccountURI, err := p.federator.AuthenticateFederatedRequest(requestedUsername, request)
- if err != nil {
- return nil, gtserror.NewErrorNotAuthorized(err)
+ requestingAccountURI, authenticated, err := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername)
+ if err != nil || !authenticated {
+ return nil, gtserror.NewErrorNotAuthorized(errors.New("not authorized"), "not authorized")
}
requestingAccount, err := p.dereferenceFediRequest(requestedUsername, requestingAccountURI)
@@ -294,7 +295,7 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
return data, nil
}
-func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode) {
+func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode) {
// get the account the request is referring to
requestedAccount := >smodel.Account{}
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
@@ -356,6 +357,5 @@ func (p *processor) GetNodeInfo(request *http.Request) (*apimodel.Nodeinfo, gtse
func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator)
- posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
- return posted, err
+ return p.federator.FederatingActor().PostInbox(contextWithChannel, w, r)
}
diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go
index 180af04fe..3cb21569a 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,17 +162,69 @@ 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
}
- return p.federateStatusDelete(statusToDelete, clientMsg.OriginAccount)
+ return p.federateStatusDelete(statusToDelete)
+ case gtsmodel.ActivityStreamsProfile, gtsmodel.ActivityStreamsPerson:
+ // DELETE ACCOUNT/PROFILE
+ accountToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Account)
+ if !ok {
+ return errors.New("account was not parseable as *gtsmodel.Account")
+ }
+
+ var deletedBy string
+ if clientMsg.OriginAccount != nil {
+ deletedBy = clientMsg.OriginAccount.ID
+ }
+
+ return p.accountProcessor.Delete(accountToDelete, deletedBy)
}
}
return nil
}
+// TODO: move all the below functions into federation.Federator
+
func (p *processor) federateStatus(status *gtsmodel.Status) error {
+ if status.GTSAuthorAccount == nil {
+ a := >smodel.Account{}
+ if err := p.db.GetByID(status.AccountID, a); err != nil {
+ return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
+ }
+ status.GTSAuthorAccount = a
+ }
+
+ // do nothing if this isn't our status
+ if status.GTSAuthorAccount.Domain != "" {
+ return nil
+ }
+
asStatus, err := p.tc.StatusToAS(status)
if err != nil {
return fmt.Errorf("federateStatus: error converting status to as format: %s", err)
@@ -186,20 +239,33 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
return err
}
-func (p *processor) federateStatusDelete(status *gtsmodel.Status, originAccount *gtsmodel.Account) error {
+func (p *processor) federateStatusDelete(status *gtsmodel.Status) error {
+ if status.GTSAuthorAccount == nil {
+ a := >smodel.Account{}
+ if err := p.db.GetByID(status.AccountID, a); err != nil {
+ return fmt.Errorf("federateStatus: error fetching status author account: %s", err)
+ }
+ status.GTSAuthorAccount = a
+ }
+
+ // do nothing if this isn't our status
+ if status.GTSAuthorAccount.Domain != "" {
+ return nil
+ }
+
asStatus, err := p.tc.StatusToAS(status)
if err != nil {
return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)
}
- outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ outboxIRI, err := url.Parse(status.GTSAuthorAccount.OutboxURI)
if err != nil {
- return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", status.GTSAuthorAccount.OutboxURI, err)
}
- actorIRI, err := url.Parse(originAccount.URI)
+ actorIRI, err := url.Parse(status.GTSAuthorAccount.URI)
if err != nil {
- return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", originAccount.URI, err)
+ return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", status.GTSAuthorAccount.URI, err)
}
// create a delete and set the appropriate actor on it
@@ -326,6 +392,11 @@ func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gts
}
func (p *processor) federateUnannounce(boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
+ if originAccount.Domain != "" {
+ // nothing to do here
+ return nil
+ }
+
asAnnounce, err := p.tc.BoostToAS(boost, originAccount, targetAccount)
if err != nil {
return fmt.Errorf("federateUnannounce: error converting status to announce: %s", err)
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index cc3ffa153..36568cf13 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -21,7 +21,6 @@ package processing
import (
"errors"
"fmt"
- "net/url"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db"
@@ -49,7 +48,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming status")
- if err := p.dereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
+ if err := p.federator.DereferenceStatusFields(incomingStatus, federatorMsg.ReceivingAccount.Username); err != nil {
return fmt.Errorf("error dereferencing status from federator: %s", err)
}
if err := p.db.UpdateByID(incomingStatus.ID, incomingStatus); err != nil {
@@ -72,7 +71,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming account")
- if err := p.dereferenceAccountFields(incomingAccount, "", false); err != nil {
+ if err := p.federator.DereferenceAccountFields(incomingAccount, "", false); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err)
}
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
@@ -105,7 +104,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return errors.New("announce was not parseable as *gtsmodel.Status")
}
- if err := p.dereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
+ if err := p.federator.DereferenceAnnounce(incomingAnnounce, federatorMsg.ReceivingAccount.Username); err != nil {
return fmt.Errorf("error dereferencing announce from federator: %s", err)
}
@@ -140,7 +139,7 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
}
l.Debug("will now derefence incoming account")
- if err := p.dereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
+ if err := p.federator.DereferenceAccountFields(incomingAccount, federatorMsg.ReceivingAccount.Username, true); err != nil {
return fmt.Errorf("error dereferencing account from federator: %s", err)
}
if err := p.db.UpdateByID(incomingAccount.ID, incomingAccount); err != nil {
@@ -160,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
@@ -183,299 +203,3 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return nil
}
-
-// dereferenceStatusFields fetches all the information we temporarily pinned to an incoming
-// federated status, back in the federating db's Create function.
-//
-// When a status comes in from the federation API, there are certain fields that
-// haven't been dereferenced yet, because we needed to provide a snappy synchronous
-// response to the caller. By the time it reaches this function though, it's being
-// processed asynchronously, so we have all the time in the world to fetch the various
-// bits and bobs that are attached to the status, and properly flesh it out, before we
-// send the status to any timelines and notify people.
-//
-// Things to dereference and fetch here:
-//
-// 1. Media attachments.
-// 2. Hashtags.
-// 3. Emojis.
-// 4. Mentions.
-// 5. Posting account.
-// 6. Replied-to-status.
-//
-// SIDE EFFECTS:
-// This function will deference all of the above, insert them in the database as necessary,
-// and attach them to the status. The status itself will not be added to the database yet,
-// that's up the caller to do.
-func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingUsername string) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "dereferenceStatusFields",
- "status": fmt.Sprintf("%+v", status),
- })
- l.Debug("entering function")
-
- t, err := p.federator.GetTransportForUser(requestingUsername)
- if err != nil {
- return fmt.Errorf("error creating transport: %s", err)
- }
-
- // the status should have an ID by now, but just in case it doesn't let's generate one here
- // because we'll need it further down
- if status.ID == "" {
- newID, err := id.NewULIDFromTime(status.CreatedAt)
- if err != nil {
- return err
- }
- status.ID = newID
- }
-
- // 1. Media attachments.
- //
- // At this point we should know:
- // * the media type of the file we're looking for (a.File.ContentType)
- // * the blurhash (a.Blurhash)
- // * the file type (a.Type)
- // * the remote URL (a.RemoteURL)
- // This should be enough to pass along to the media processor.
- attachmentIDs := []string{}
- for _, a := range status.GTSMediaAttachments {
- l.Debugf("dereferencing attachment: %+v", a)
-
- // it might have been processed elsewhere so check first if it's already in the database or not
- maybeAttachment := >smodel.MediaAttachment{}
- err := p.db.GetWhere([]db.Where{{Key: "remote_url", Value: a.RemoteURL}}, maybeAttachment)
- if err == nil {
- // we already have it in the db, dereferenced, no need to do it again
- l.Debugf("attachment already exists with id %s", maybeAttachment.ID)
- attachmentIDs = append(attachmentIDs, maybeAttachment.ID)
- continue
- }
- if _, ok := err.(db.ErrNoEntries); !ok {
- // we have a real error
- return fmt.Errorf("error checking db for existence of attachment with remote url %s: %s", a.RemoteURL, err)
- }
- // it just doesn't exist yet so carry on
- l.Debug("attachment doesn't exist yet, calling ProcessRemoteAttachment", a)
- deferencedAttachment, err := p.mediaHandler.ProcessRemoteAttachment(t, a, status.AccountID)
- if err != nil {
- p.log.Errorf("error dereferencing status attachment: %s", err)
- continue
- }
- l.Debugf("dereferenced attachment: %+v", deferencedAttachment)
- deferencedAttachment.StatusID = status.ID
- deferencedAttachment.Description = a.Description
- if err := p.db.Put(deferencedAttachment); err != nil {
- return fmt.Errorf("error inserting dereferenced attachment with remote url %s: %s", a.RemoteURL, err)
- }
- attachmentIDs = append(attachmentIDs, deferencedAttachment.ID)
- }
- status.Attachments = attachmentIDs
-
- // 2. Hashtags
-
- // 3. Emojis
-
- // 4. Mentions
- // At this point, mentions should have the namestring and mentionedAccountURI set on them.
- //
- // We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
- mentions := []string{}
- for _, m := range status.GTSMentions {
- if m.ID == "" {
- mID, err := id.NewRandomULID()
- if err != nil {
- return err
- }
- m.ID = mID
- }
-
- uri, err := url.Parse(m.MentionedAccountURI)
- if err != nil {
- l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
- continue
- }
-
- m.StatusID = status.ID
- m.OriginAccountID = status.GTSAuthorAccount.ID
- m.OriginAccountURI = status.GTSAuthorAccount.URI
-
- targetAccount := >smodel.Account{}
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, targetAccount); err != nil {
- // proper error
- if _, ok := err.(db.ErrNoEntries); !ok {
- return fmt.Errorf("db error checking for account with uri %s", uri.String())
- }
-
- // we just don't have it yet, so we should go get it....
- accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, uri)
- if err != nil {
- // we can't dereference it so just skip it
- l.Debugf("error dereferencing remote account with uri %s: %s", uri.String(), err)
- continue
- }
-
- targetAccount, err = p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- l.Debugf("error converting remote account with uri %s into gts model: %s", uri.String(), err)
- continue
- }
-
- targetAccountID, err := id.NewRandomULID()
- if err != nil {
- return err
- }
- targetAccount.ID = targetAccountID
-
- if err := p.db.Put(targetAccount); err != nil {
- return fmt.Errorf("db error inserting account with uri %s", uri.String())
- }
- }
-
- // by this point, we know the targetAccount exists in our database with an ID :)
- m.TargetAccountID = targetAccount.ID
- if err := p.db.Put(m); err != nil {
- return fmt.Errorf("error creating mention: %s", err)
- }
- mentions = append(mentions, m.ID)
- }
- status.Mentions = mentions
-
- return nil
-}
-
-func (p *processor) dereferenceAccountFields(account *gtsmodel.Account, requestingUsername string, refresh bool) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "dereferenceAccountFields",
- "requestingUsername": requestingUsername,
- })
-
- t, err := p.federator.GetTransportForUser(requestingUsername)
- if err != nil {
- return fmt.Errorf("error getting transport for user: %s", err)
- }
-
- // fetch the header and avatar
- if err := p.fetchHeaderAndAviForAccount(account, t, refresh); err != nil {
- // if this doesn't work, just skip it -- we can do it later
- l.Debugf("error fetching header/avi for account: %s", err)
- }
-
- if err := p.db.UpdateByID(account.ID, account); err != nil {
- return fmt.Errorf("error updating account in database: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUsername string) error {
- if announce.GTSBoostedStatus == nil || announce.GTSBoostedStatus.URI == "" {
- // we can't do anything unfortunately
- return errors.New("dereferenceAnnounce: no URI to dereference")
- }
-
- // check if we already have the boosted status in the database
- boostedStatus := >smodel.Status{}
- err := p.db.GetWhere([]db.Where{{Key: "uri", Value: announce.GTSBoostedStatus.URI}}, boostedStatus)
- if err == nil {
- // nice, we already have it so we don't actually need to dereference it from remote
- announce.Content = boostedStatus.Content
- announce.ContentWarning = boostedStatus.ContentWarning
- announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
- announce.Sensitive = boostedStatus.Sensitive
- announce.Language = boostedStatus.Language
- announce.Text = boostedStatus.Text
- announce.BoostOfID = boostedStatus.ID
- announce.Visibility = boostedStatus.Visibility
- announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
- announce.GTSBoostedStatus = boostedStatus
- return nil
- }
-
- // we don't have it so we need to dereference it
- remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // make sure we have the author account in the db
- attributedToProp := statusable.GetActivityStreamsAttributedTo()
- for iter := attributedToProp.Begin(); iter != attributedToProp.End(); iter = iter.Next() {
- accountURI := iter.GetIRI()
- if accountURI == nil {
- continue
- }
-
- if err := p.db.GetWhere([]db.Where{{Key: "uri", Value: accountURI.String()}}, >smodel.Account{}); err == nil {
- // we already have it, fine
- continue
- }
-
- // we don't have the boosted status author account yet so dereference it
- accountable, err := p.federator.DereferenceRemoteAccount(requestingUsername, accountURI)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing remote account with id %s: %s", accountURI.String(), err)
- }
- account, err := p.tc.ASRepresentationToAccount(accountable, false)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
- }
-
- accountID, err := id.NewRandomULID()
- if err != nil {
- return err
- }
- account.ID = accountID
-
- if err := p.db.Put(account); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
- }
-
- if err := p.dereferenceAccountFields(account, requestingUsername, false); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing fields on account with id %s : %s", accountURI.String(), err)
- }
- }
-
- // now convert the statusable into something we can understand
- boostedStatus, err = p.tc.ASStatusToStatus(statusable)
- if err != nil {
- return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
- }
-
- boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
- if err != nil {
- return nil
- }
- boostedStatus.ID = boostedStatusID
-
- if err := p.db.Put(boostedStatus); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // now dereference additional fields straight away (we're already async here so we have time)
- if err := p.dereferenceStatusFields(boostedStatus, requestingUsername); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error dereferencing status fields for status with id %s: %s", announce.GTSBoostedStatus.URI, err)
- }
-
- // update with the newly dereferenced fields
- if err := p.db.UpdateByID(boostedStatus.ID, boostedStatus); err != nil {
- return fmt.Errorf("dereferenceAnnounce: error updating dereferenced status in the db: %s", err)
- }
-
- // we have everything we need!
- announce.Content = boostedStatus.Content
- announce.ContentWarning = boostedStatus.ContentWarning
- announce.ActivityStreamsType = boostedStatus.ActivityStreamsType
- announce.Sensitive = boostedStatus.Sensitive
- announce.Language = boostedStatus.Language
- announce.Text = boostedStatus.Text
- announce.BoostOfID = boostedStatus.ID
- announce.Visibility = boostedStatus.Visibility
- announce.VisibilityAdvanced = boostedStatus.VisibilityAdvanced
- announce.GTSBoostedStatus = boostedStatus
- return nil
-}
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index a9b2fbd96..d0b66cc2f 100644
--- a/internal/processing/instance.go
+++ b/internal/processing/instance.go
@@ -131,7 +131,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
// process avatar if provided
if form.Avatar != nil && form.Avatar.Size != 0 {
- _, err := p.updateAccountAvatar(form.Avatar, ia.ID)
+ _, err := p.accountProcessor.UpdateAvatar(form.Avatar, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing avatar")
}
@@ -139,7 +139,7 @@ func (p *processor) InstancePatch(form *apimodel.InstanceSettingsUpdateRequest)
// process header if provided
if form.Header != nil && form.Header.Size != 0 {
- _, err := p.updateAccountHeader(form.Header, ia.ID)
+ _, err := p.accountProcessor.UpdateHeader(form.Header, ia.ID)
if err != nil {
return nil, gtserror.NewErrorBadRequest(err, "error processing header")
}
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..1664306b8
--- /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"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/media"
+)
+
+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..aa3583054
--- /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"
+
+ 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) 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 566bec8e5..d24897f61 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -21,6 +21,7 @@ package processing
import (
"context"
"net/http"
+ "net/url"
"github.com/sirupsen/logrus"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
@@ -32,8 +33,11 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status"
- "github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/streaming"
+ "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"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/visibility"
@@ -81,6 +85,14 @@ type Processor interface {
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
+ // AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form.
+ AdminDomainBlockCreate(authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode)
+ // AdminDomainBlocksGet returns a list of currently blocked domains.
+ AdminDomainBlocksGet(authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode)
+ // AdminDomainBlockGet returns one domain block, specified by ID.
+ AdminDomainBlockGet(authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode)
+ // AdminDomainBlockDelete deletes one domain block, specified by ID, returning the deleted domain block.
+ AdminDomainBlockDelete(authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode)
// AppCreate processes the creation of a new API application
AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, error)
@@ -154,22 +166,22 @@ type Processor interface {
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
// before returning a JSON serializable interface to the caller.
- GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
+ GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
- GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
+ GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
- GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
+ GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
// authentication before returning a JSON serializable interface to the caller.
- GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode)
+ GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode)
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
- GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
+ GetWebfingerAccount(ctx context.Context, requestedUsername string, requestURL *url.URL) (*apimodel.WellKnownResponse, gtserror.WithCode)
// GetNodeInfoRel returns a well known response giving the path to node info.
GetNodeInfoRel(request *http.Request) (*apimodel.WellKnownResponse, gtserror.WithCode)
@@ -210,8 +222,11 @@ type processor struct {
SUB-PROCESSORS
*/
+ accountProcessor account.Processor
+ adminProcessor admin.Processor
statusProcessor status.Processor
streamingProcessor streaming.Processor
+ mediaProcessor mediaProcessor.Processor
}
// NewProcessor returns a new Processor that uses the given federator and logger
@@ -222,6 +237,9 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
statusProcessor := status.New(db, tc, config, fromClientAPI, log)
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,
@@ -238,8 +256,11 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f
db: db,
filter: visibility.NewFilter(db, log),
+ accountProcessor: accountProcessor,
+ adminProcessor: adminProcessor,
statusProcessor: statusProcessor,
streamingProcessor: streamingProcessor,
+ mediaProcessor: mediaProcessor,
}
}
@@ -251,14 +272,18 @@ func (p *processor) Start() error {
select {
case clientMsg := <-p.fromClientAPI:
p.log.Infof("received message FROM client API: %+v", clientMsg)
- if err := p.processFromClientAPI(clientMsg); err != nil {
- p.log.Error(err)
- }
+ go func() {
+ if err := p.processFromClientAPI(clientMsg); err != nil {
+ p.log.Error(err)
+ }
+ }()
case federatorMsg := <-p.fromFederator:
p.log.Infof("received message FROM federator: %+v", federatorMsg)
- if err := p.processFromFederator(federatorMsg); err != nil {
- p.log.Error(err)
- }
+ go func() {
+ if err := p.processFromFederator(federatorMsg); err != nil {
+ p.log.Error(err)
+ }
+ }()
case <-p.stop:
break DistLoop
}
diff --git a/internal/processing/search.go b/internal/processing/search.go
index a0a48145b..727ad13bd 100644
--- a/internal/processing/search.go
+++ b/internal/processing/search.go
@@ -176,7 +176,7 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve
}
// properly dereference everything in the status (media attachments etc)
- if err := p.dereferenceStatusFields(status, authed.Account.Username); err != nil {
+ if err := p.federator.DereferenceStatusFields(status, authed.Account.Username); err != nil {
return nil, fmt.Errorf("error dereferencing status fields: %s", err)
}
@@ -223,7 +223,7 @@ func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
}
- if err := p.dereferenceAccountFields(account, authed.Account.Username, false); err != nil {
+ if err := p.federator.DereferenceAccountFields(account, authed.Account.Username, false); err != nil {
return nil, fmt.Errorf("searchAccountByURI: error further dereferencing account with uri %s: %s", uri.String(), err)
}
@@ -301,7 +301,7 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r
}
// properly dereference all the fields on the account immediately
- if err := p.dereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
+ if err := p.federator.DereferenceAccountFields(foundAccount, authed.Account.Username, true); err != nil {
return nil, fmt.Errorf("searchAccountByMention: error dereferencing fields on account with uri %s: %s", acctURI.String(), err)
}
}
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
new file mode 100644
index 000000000..93d0f19de
--- /dev/null
+++ b/internal/processing/status/boost.go
@@ -0,0 +1,73 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusBoost")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Boostable {
+ return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))
+ }
+ }
+
+ // it's visible! it's boostable! so let's boost the FUCK out of it
+ boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ boostWrapperStatus.CreatedWithApplicationID = application.ID
+ boostWrapperStatus.GTSBoostedAccount = targetAccount
+
+ // put the boost in the database
+ if err := p.db.Put(boostWrapperStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: boostWrapperStatus,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go
new file mode 100644
index 000000000..b352178e3
--- /dev/null
+++ b/internal/processing/status/boostedby.go
@@ -0,0 +1,68 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusBoostedBy")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible"))
+ }
+
+ // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
+ favingAccounts, err := p.db.WhoBoostedStatus(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))
+ }
+
+ // filter the list so the user doesn't see accounts they blocked or which blocked them
+ filteredAccounts := []*gtsmodel.Account{}
+ for _, acc := range favingAccounts {
+ blocked, err := p.db.Blocked(account.ID, acc.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))
+ }
+ if !blocked {
+ filteredAccounts = append(filteredAccounts, acc)
+ }
+ }
+
+ // TODO: filter other things here? suspended? muted? silenced?
+
+ // now we can return the masto representation of those accounts
+ mastoAccounts := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ mastoAccount, err := p.tc.AccountToMastoPublic(acc)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
new file mode 100644
index 000000000..72b9b5623
--- /dev/null
+++ b/internal/processing/status/context.go
@@ -0,0 +1,69 @@
+package status
+
+import (
+ "fmt"
+ "sort"
+
+ 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) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
+
+ context := &apimodel.Context{
+ Ancestors: []apimodel.Status{},
+ Descendants: []apimodel.Status{},
+ }
+
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ if !visible {
+ return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID))
+ }
+
+ parents, err := p.db.StatusParents(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range parents {
+ if v, err := p.filter.StatusVisible(status, account); err == nil && v {
+ mastoStatus, err := p.tc.StatusToMasto(status, account)
+ if err == nil {
+ context.Ancestors = append(context.Ancestors, *mastoStatus)
+ }
+ }
+ }
+
+ sort.Slice(context.Ancestors, func(i int, j int) bool {
+ return context.Ancestors[i].ID < context.Ancestors[j].ID
+ })
+
+ children, err := p.db.StatusChildren(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range children {
+ if v, err := p.filter.StatusVisible(status, account); err == nil && v {
+ mastoStatus, err := p.tc.StatusToMasto(status, account)
+ if err == nil {
+ context.Descendants = append(context.Descendants, *mastoStatus)
+ }
+ }
+ }
+
+ return context, nil
+}
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
new file mode 100644
index 000000000..aa7468ae5
--- /dev/null
+++ b/internal/processing/status/create.go
@@ -0,0 +1,106 @@
+package status
+
+import (
+ "fmt"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
+ uris := util.GenerateURIsForAccount(account.Username, p.config.Protocol, p.config.Host)
+ thisStatusID, err := id.NewULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
+ thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
+
+ newStatus := >smodel.Status{
+ ID: thisStatusID,
+ URI: thisStatusURI,
+ URL: thisStatusURL,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ Local: true,
+ AccountID: account.ID,
+ AccountURI: account.URI,
+ ContentWarning: form.SpoilerText,
+ ActivityStreamsType: gtsmodel.ActivityStreamsNote,
+ Sensitive: form.Sensitive,
+ Language: form.Language,
+ CreatedWithApplicationID: application.ID,
+ Text: form.Status,
+ }
+
+ // check if replyToID is ok
+ if err := p.processReplyToID(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // check if mediaIDs are ok
+ if err := p.processMediaIDs(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // check if visibility settings are ok
+ if err := p.processVisibility(form, account.Privacy, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // handle language settings
+ if err := p.processLanguage(form, account.Language, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // handle mentions
+ if err := p.processMentions(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processTags(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processEmojis(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if err := p.processContent(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // put the new status in the database, generating an ID for it in the process
+ if err := p.db.Put(newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // change the status ID of the media attachments to the new status
+ for _, a := range newStatus.GTSMediaAttachments {
+ a.StatusID = newStatus.ID
+ a.UpdatedAt = time.Now()
+ if err := p.db.UpdateByID(a.ID, a); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: newStatus,
+ OriginAccount: account,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(newStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go
new file mode 100644
index 000000000..259038dee
--- /dev/null
+++ b/internal/processing/status/delete.go
@@ -0,0 +1,56 @@
+package status
+
+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) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusDelete")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ // status is already gone
+ return nil, nil
+ }
+
+ if targetStatus.AccountID != account.ID {
+ return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ if err := p.db.DeleteByID(targetStatus.ID, >smodel.Status{}); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsDelete,
+ GTSModel: targetStatus,
+ OriginAccount: account,
+ TargetAccount: account,
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
new file mode 100644
index 000000000..23f0d2944
--- /dev/null
+++ b/internal/processing/status/fave.go
@@ -0,0 +1,101 @@
+package status
+
+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"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusFave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // is the status faveable?
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Likeable {
+ return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
+ }
+ }
+
+ // first check if the status is already faved, if so we don't need to do anything
+ newFave := true
+ gtsFave := >smodel.StatusFave{}
+ if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil {
+ // we already have a fave for this status
+ newFave = false
+ }
+
+ if newFave {
+ thisFaveID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // we need to create a new fave in the database
+ gtsFave := >smodel.StatusFave{
+ ID: thisFaveID,
+ AccountID: account.ID,
+ TargetAccountID: targetAccount.ID,
+ StatusID: targetStatus.ID,
+ URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID),
+ GTSStatus: targetStatus,
+ GTSTargetAccount: targetAccount,
+ GTSFavingAccount: account,
+ }
+
+ if err := p.db.Put(gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ // return the mastodon representation of the target status
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go
new file mode 100644
index 000000000..5194cc258
--- /dev/null
+++ b/internal/processing/status/favedby.go
@@ -0,0 +1,68 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusFavedBy")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
+ favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
+ }
+
+ // filter the list so the user doesn't see accounts they blocked or which blocked them
+ filteredAccounts := []*gtsmodel.Account{}
+ for _, acc := range favingAccounts {
+ blocked, err := p.db.Blocked(account.ID, acc.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
+ }
+ if !blocked {
+ filteredAccounts = append(filteredAccounts, acc)
+ }
+ }
+
+ // TODO: filter other things here? suspended? muted? silenced?
+
+ // now we can return the masto representation of those accounts
+ mastoAccounts := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ mastoAccount, err := p.tc.AccountToMastoPublic(acc)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go
new file mode 100644
index 000000000..9a70185b0
--- /dev/null
+++ b/internal/processing/status/get.go
@@ -0,0 +1,52 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusGet")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = >smodel.Status{}
+ if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+
+}
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
new file mode 100644
index 000000000..d83c325fd
--- /dev/null
+++ b/internal/processing/status/status.go
@@ -0,0 +1,57 @@
+package status
+
+import (
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "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/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+)
+
+// Processor wraps a bunch of functions for processing statuses.
+type Processor interface {
+ // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
+ Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
+ // Delete processes the delete of a given status, returning the deleted status if the delete goes through.
+ Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Fave processes the faving of a given status, returning the updated status if the fave goes through.
+ Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
+ Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
+ Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
+ BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
+ // FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
+ FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
+ // Get gets the given status, taking account of privacy settings and blocks etc.
+ Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Unfave processes the unfaving of a given status, returning the updated status if the fave goes through.
+ Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
+ // Context returns the context (previous and following posts) from the given status ID
+ Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ db db.DB
+ filter visibility.Filter
+ fromClientAPI chan gtsmodel.FromClientAPI
+ log *logrus.Logger
+}
+
+// New returns a new status processor.
+func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan gtsmodel.FromClientAPI, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ db: db,
+ filter: visibility.NewFilter(db, log),
+ fromClientAPI: fromClientAPI,
+ log: log,
+ }
+}
diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go
new file mode 100644
index 000000000..2a1394695
--- /dev/null
+++ b/internal/processing/status/unboost.go
@@ -0,0 +1,95 @@
+package status
+
+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) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "Unboost")
+
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // check if we actually have a boost for this status
+ var toUnboost bool
+
+ gtsBoost := >smodel.Status{}
+ where := []db.Where{
+ {
+ Key: "boost_of_id",
+ Value: targetStatusID,
+ },
+ {
+ Key: "account_id",
+ Value: account.ID,
+ },
+ }
+ err = p.db.GetWhere(where, gtsBoost)
+ if err == nil {
+ // we have a boost
+ toUnboost = true
+ }
+
+ if err != nil {
+ // something went wrong in the db finding the boost
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
+ }
+ // we just don't have a boost
+ toUnboost = false
+ }
+
+ if toUnboost {
+ // we had a boost, so take some action to get rid of it
+ if err := p.db.DeleteWhere(where, >smodel.Status{}); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unboosting status: %s", err))
+ }
+
+ // pin some stuff onto the boost while we have it out of the db
+ gtsBoost.GTSBoostedStatus = targetStatus
+ gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount
+ gtsBoost.GTSBoostedAccount = targetAccount
+ gtsBoost.GTSAuthorAccount = account
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: gtsBoost,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go
new file mode 100644
index 000000000..b51daacb9
--- /dev/null
+++ b/internal/processing/status/unfave.go
@@ -0,0 +1,77 @@
+package status
+
+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) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusUnfave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := >smodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+
+ l.Tracef("going to search for target account %s", targetStatus.AccountID)
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
+ }
+
+ l.Trace("going to see if status is visible")
+ visible, err := p.filter.StatusVisible(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // check if we actually have a fave for this status
+ var toUnfave bool
+
+ gtsFave := >smodel.StatusFave{}
+ err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave)
+ if err == nil {
+ // we have a fave
+ toUnfave = true
+ }
+ if err != nil {
+ // something went wrong in the db finding the fave
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
+ }
+ // we just don't have a fave
+ toUnfave = false
+ }
+
+ if toUnfave {
+ // we had a fave, so take some action to get rid of it
+ if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go
new file mode 100644
index 000000000..0a023eab6
--- /dev/null
+++ b/internal/processing/status/util.go
@@ -0,0 +1,269 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
+ // by default all flags are set to true
+ gtsAdvancedVis := >smodel.VisibilityAdvanced{
+ Federated: true,
+ Boostable: true,
+ Replyable: true,
+ Likeable: true,
+ }
+
+ var gtsBasicVis gtsmodel.Visibility
+ // Advanced takes priority if it's set.
+ // If it's not set, take whatever masto visibility is set.
+ // If *that's* not set either, then just take the account default.
+ // If that's also not set, take the default for the whole instance.
+ if form.VisibilityAdvanced != nil {
+ gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
+ } else if form.Visibility != "" {
+ gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
+ } else if accountDefaultVis != "" {
+ gtsBasicVis = accountDefaultVis
+ } else {
+ gtsBasicVis = gtsmodel.VisibilityDefault
+ }
+
+ switch gtsBasicVis {
+ case gtsmodel.VisibilityPublic:
+ // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
+ break
+ case gtsmodel.VisibilityUnlocked:
+ // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
+ if form.Federated != nil {
+ gtsAdvancedVis.Federated = *form.Federated
+ }
+
+ if form.Boostable != nil {
+ gtsAdvancedVis.Boostable = *form.Boostable
+ }
+
+ if form.Replyable != nil {
+ gtsAdvancedVis.Replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ gtsAdvancedVis.Likeable = *form.Likeable
+ }
+
+ case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
+ // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
+ gtsAdvancedVis.Boostable = false
+
+ if form.Federated != nil {
+ gtsAdvancedVis.Federated = *form.Federated
+ }
+
+ if form.Replyable != nil {
+ gtsAdvancedVis.Replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ gtsAdvancedVis.Likeable = *form.Likeable
+ }
+
+ case gtsmodel.VisibilityDirect:
+ // direct is pretty easy: there's only one possible setting so return it
+ gtsAdvancedVis.Federated = true
+ gtsAdvancedVis.Boostable = false
+ gtsAdvancedVis.Federated = true
+ gtsAdvancedVis.Likeable = true
+ }
+
+ status.Visibility = gtsBasicVis
+ status.VisibilityAdvanced = gtsAdvancedVis
+ return nil
+}
+
+func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
+ if form.InReplyToID == "" {
+ return nil
+ }
+
+ // If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
+ //
+ // 1. Does the replied status exist in the database?
+ // 2. Is the replied status marked as replyable?
+ // 3. Does a block exist between either the current account or the account that posted the status it's replying to?
+ //
+ // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
+ repliedStatus := >smodel.Status{}
+ repliedAccount := >smodel.Account{}
+ // check replied status exists + is replyable
+ if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
+ }
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+
+ if repliedStatus.VisibilityAdvanced != nil {
+ if !repliedStatus.VisibilityAdvanced.Replyable {
+ return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
+ }
+ }
+
+ // check replied account is known to us
+ if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
+ }
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+ // check if a block exists
+ if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
+ }
+ } else if blocked {
+ return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
+ }
+ status.InReplyToID = repliedStatus.ID
+ status.InReplyToAccountID = repliedAccount.ID
+
+ return nil
+}
+
+func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
+ if form.MediaIDs == nil {
+ return nil
+ }
+
+ gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
+ attachments := []string{}
+ for _, mediaID := range form.MediaIDs {
+ // check these attachments exist
+ a := >smodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaID, a); err != nil {
+ return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
+ }
+ // check they belong to the requesting account id
+ if a.AccountID != thisAccountID {
+ return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
+ }
+ // check they're not already used in a status
+ if a.StatusID != "" || a.ScheduledStatusID != "" {
+ return fmt.Errorf("media with id %s is already attached to a status", mediaID)
+ }
+ gtsMediaAttachments = append(gtsMediaAttachments, a)
+ attachments = append(attachments, a.ID)
+ }
+ status.GTSMediaAttachments = gtsMediaAttachments
+ status.Attachments = attachments
+ return nil
+}
+
+func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
+ if form.Language != "" {
+ status.Language = form.Language
+ } else {
+ status.Language = accountDefaultLanguage
+ }
+ if status.Language == "" {
+ return errors.New("no language given either in status create form or account default")
+ }
+ return nil
+}
+
+func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ menchies := []string{}
+ gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating mentions from status: %s", err)
+ }
+ for _, menchie := range gtsMenchies {
+ menchieID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ menchie.ID = menchieID
+
+ if err := p.db.Put(menchie); err != nil {
+ return fmt.Errorf("error putting mentions in db: %s", err)
+ }
+ menchies = append(menchies, menchie.ID)
+ }
+ // add full populated gts menchies to the status for passing them around conveniently
+ status.GTSMentions = gtsMenchies
+ // add just the ids of the mentioned accounts to the status for putting in the db
+ status.Mentions = menchies
+ return nil
+}
+
+func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ tags := []string{}
+ gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating hashtags from status: %s", err)
+ }
+ for _, tag := range gtsTags {
+ if err := p.db.Upsert(tag, "name"); err != nil {
+ return fmt.Errorf("error putting tags in db: %s", err)
+ }
+ tags = append(tags, tag.ID)
+ }
+ // add full populated gts tags to the status for passing them around conveniently
+ status.GTSTags = gtsTags
+ // add just the ids of the used tags to the status for putting in the db
+ status.Tags = tags
+ return nil
+}
+
+func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ emojis := []string{}
+ gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating emojis from status: %s", err)
+ }
+ for _, e := range gtsEmojis {
+ emojis = append(emojis, e.ID)
+ }
+ // add full populated gts emojis to the status for passing them around conveniently
+ status.GTSEmojis = gtsEmojis
+ // add just the ids of the used emojis to the status for putting in the db
+ status.Emojis = emojis
+ return nil
+}
+
+func (p *processor) processContent(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ if form.Status == "" {
+ status.Content = ""
+ return nil
+ }
+
+ // surround the whole status in '
'
+ content := fmt.Sprintf(`
%s
`, form.Status)
+
+ // format mentions nicely
+ for _, menchie := range status.GTSMentions {
+ targetAccount := >smodel.Account{}
+ if err := p.db.GetByID(menchie.TargetAccountID, targetAccount); err == nil {
+ mentionContent := fmt.Sprintf(`@%s`, targetAccount.URL, targetAccount.Username)
+ content = strings.ReplaceAll(content, menchie.NameString, mentionContent)
+ }
+ }
+
+ // format tags nicely
+ for _, tag := range status.GTSTags {
+ tagContent := fmt.Sprintf(`#%s`, tag.URL, tag.Name)
+ content = strings.ReplaceAll(content, fmt.Sprintf("#%s", tag.Name), tagContent)
+ }
+
+ // replace newlines with breaks
+ content = strings.ReplaceAll(content, "\n", "
")
+
+ status.Content = content
+ return nil
+}
diff --git a/internal/processing/streaming/authorize.go b/internal/processing/streaming/authorize.go
new file mode 100644
index 000000000..8bbf1856d
--- /dev/null
+++ b/internal/processing/streaming/authorize.go
@@ -0,0 +1,33 @@
+package streaming
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error) {
+ ti, err := p.oauthServer.LoadAccessToken(context.Background(), accessToken)
+ if err != nil {
+ return nil, fmt.Errorf("AuthorizeStreamingRequest: error loading access token: %s", err)
+ }
+
+ uid := ti.GetUserID()
+ if uid == "" {
+ return nil, fmt.Errorf("AuthorizeStreamingRequest: no userid in token")
+ }
+
+ // fetch user's and account for this user id
+ user := >smodel.User{}
+ if err := p.db.GetByID(uid, user); err != nil || user == nil {
+ return nil, fmt.Errorf("AuthorizeStreamingRequest: no user found for validated uid %s", uid)
+ }
+
+ acct := >smodel.Account{}
+ if err := p.db.GetByID(user.AccountID, acct); err != nil || acct == nil {
+ return nil, fmt.Errorf("AuthorizeStreamingRequest: no account retrieved for user with id %s", uid)
+ }
+
+ return acct, nil
+}
diff --git a/internal/processing/streaming/openstream.go b/internal/processing/streaming/openstream.go
new file mode 100644
index 000000000..68446bac6
--- /dev/null
+++ b/internal/processing/streaming/openstream.go
@@ -0,0 +1,100 @@
+package streaming
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+)
+
+func (p *processor) OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "OpenStreamForAccount",
+ "account": account.ID,
+ "streamType": streamType,
+ })
+ l.Debug("received open stream request")
+
+ // each stream needs a unique ID so we know to close it
+ streamID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error generating stream id: %s", err))
+ }
+
+ thisStream := >smodel.Stream{
+ ID: streamID,
+ Type: streamType,
+ Messages: make(chan *gtsmodel.Message, 100),
+ Hangup: make(chan interface{}, 1),
+ Connected: true,
+ }
+ go p.waitToCloseStream(account, thisStream)
+
+ v, ok := p.streamMap.Load(account.ID)
+ if !ok || v == nil {
+ // there is no entry in the streamMap for this account yet, so make one and store it
+ streamsForAccount := >smodel.StreamsForAccount{
+ Streams: []*gtsmodel.Stream{
+ thisStream,
+ },
+ }
+ p.streamMap.Store(account.ID, streamsForAccount)
+ } else {
+ // there is an entry in the streamMap for this account
+ // parse the interface as a streamsForAccount
+ streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
+ if !ok {
+ return nil, gtserror.NewErrorInternalError(errors.New("stream map error"))
+ }
+
+ // append this stream to it
+ streamsForAccount.Lock()
+ streamsForAccount.Streams = append(streamsForAccount.Streams, thisStream)
+ streamsForAccount.Unlock()
+ }
+
+ return thisStream, nil
+}
+
+// waitToCloseStream waits until the hangup channel is closed for the given stream.
+// It then iterates through the map of streams stored by the processor, removes the stream from it,
+// and then closes the messages channel of the stream to indicate that the channel should no longer be read from.
+func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *gtsmodel.Stream) {
+ <-thisStream.Hangup // wait for a hangup message
+
+ // lock the stream to prevent more messages being put in it while we work
+ thisStream.Lock()
+ defer thisStream.Unlock()
+
+ // indicate the stream is no longer connected
+ thisStream.Connected = false
+
+ // load and parse the entry for this account from the stream map
+ v, ok := p.streamMap.Load(account.ID)
+ if !ok || v == nil {
+ return
+ }
+ streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
+ if !ok {
+ return
+ }
+
+ // lock the streams for account while we remove this stream from its slice
+ streamsForAccount.Lock()
+ defer streamsForAccount.Unlock()
+
+ // put everything into modified streams *except* the stream we're removing
+ modifiedStreams := []*gtsmodel.Stream{}
+ for _, s := range streamsForAccount.Streams {
+ if s.ID != thisStream.ID {
+ modifiedStreams = append(modifiedStreams, s)
+ }
+ }
+ streamsForAccount.Streams = modifiedStreams
+
+ // finally close the messages channel so no more messages can be read from it
+ close(thisStream.Messages)
+}
diff --git a/internal/processing/streaming/streamdelete.go b/internal/processing/streaming/streamdelete.go
new file mode 100644
index 000000000..2282c29ae
--- /dev/null
+++ b/internal/processing/streaming/streamdelete.go
@@ -0,0 +1,51 @@
+package streaming
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) StreamDelete(statusID string) error {
+ errs := []string{}
+
+ // we want to range through ALL streams for ALL accounts here to make sure it's very clear to everyone that the status has been deleted
+ p.streamMap.Range(func(k interface{}, v interface{}) bool {
+ // the key of this map should be an accountID (string)
+ accountID, ok := k.(string)
+ if !ok {
+ errs = append(errs, "key in streamMap was not a string!")
+ return false
+ }
+
+ // the value of the map should be a buncha streams
+ streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
+ if !ok {
+ errs = append(errs, fmt.Sprintf("stream map error for account stream %s", accountID))
+ }
+
+ // lock the streams while we work on them
+ streamsForAccount.Lock()
+ defer streamsForAccount.Unlock()
+ for _, stream := range streamsForAccount.Streams {
+ // lock each individual stream as we work on it
+ stream.Lock()
+ defer stream.Unlock()
+ if stream.Connected {
+ stream.Messages <- >smodel.Message{
+ Stream: []string{stream.Type},
+ Event: "delete",
+ Payload: statusID,
+ }
+ }
+ }
+ return true
+ })
+
+ if len(errs) != 0 {
+ return fmt.Errorf("one or more errors streaming status delete: %s", strings.Join(errs, ";"))
+ }
+
+ return nil
+}
diff --git a/internal/processing/streaming/streaming.go b/internal/processing/streaming/streaming.go
new file mode 100644
index 000000000..de75b8f27
--- /dev/null
+++ b/internal/processing/streaming/streaming.go
@@ -0,0 +1,52 @@
+package streaming
+
+import (
+ "sync"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "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/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/visibility"
+)
+
+// Processor wraps a bunch of functions for processing streaming.
+type Processor interface {
+ // AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API
+ AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error)
+ // OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller.
+ OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode)
+ // StreamStatusToAccount streams the given status to any open, appropriate streams belonging to the given account.
+ StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error
+ // StreamNotificationToAccount streams the given notification to any open, appropriate streams belonging to the given account.
+ StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error
+ // StreamDelete streams the delete of the given statusID to *ALL* open streams.
+ StreamDelete(statusID string) error
+}
+
+type processor struct {
+ tc typeutils.TypeConverter
+ config *config.Config
+ db db.DB
+ filter visibility.Filter
+ log *logrus.Logger
+ oauthServer oauth.Server
+ streamMap *sync.Map
+}
+
+// New returns a new status processor.
+func New(db db.DB, tc typeutils.TypeConverter, oauthServer oauth.Server, config *config.Config, log *logrus.Logger) Processor {
+ return &processor{
+ tc: tc,
+ config: config,
+ db: db,
+ filter: visibility.NewFilter(db, log),
+ log: log,
+ oauthServer: oauthServer,
+ streamMap: &sync.Map{},
+ }
+}
diff --git a/internal/processing/streaming/streamnotification.go b/internal/processing/streaming/streamnotification.go
new file mode 100644
index 000000000..24c8342ee
--- /dev/null
+++ b/internal/processing/streaming/streamnotification.go
@@ -0,0 +1,50 @@
+package streaming
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "StreamNotificationToAccount",
+ "account": account.ID,
+ })
+ v, ok := p.streamMap.Load(account.ID)
+ if !ok {
+ // no open connections so nothing to stream
+ return nil
+ }
+
+ streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
+ if !ok {
+ return errors.New("stream map error")
+ }
+
+ notificationBytes, err := json.Marshal(n)
+ if err != nil {
+ return fmt.Errorf("error marshalling notification to json: %s", err)
+ }
+
+ streamsForAccount.Lock()
+ defer streamsForAccount.Unlock()
+ for _, stream := range streamsForAccount.Streams {
+ stream.Lock()
+ defer stream.Unlock()
+ if stream.Connected {
+ l.Debugf("streaming notification to stream id %s", stream.ID)
+ stream.Messages <- >smodel.Message{
+ Stream: []string{stream.Type},
+ Event: "notification",
+ Payload: string(notificationBytes),
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/processing/streaming/streamstatus.go b/internal/processing/streaming/streamstatus.go
new file mode 100644
index 000000000..8d026252d
--- /dev/null
+++ b/internal/processing/streaming/streamstatus.go
@@ -0,0 +1,50 @@
+package streaming
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/sirupsen/logrus"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "StreamStatusForAccount",
+ "account": account.ID,
+ })
+ v, ok := p.streamMap.Load(account.ID)
+ if !ok {
+ // no open connections so nothing to stream
+ return nil
+ }
+
+ streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
+ if !ok {
+ return errors.New("stream map error")
+ }
+
+ statusBytes, err := json.Marshal(s)
+ if err != nil {
+ return fmt.Errorf("error marshalling status to json: %s", err)
+ }
+
+ streamsForAccount.Lock()
+ defer streamsForAccount.Unlock()
+ for _, stream := range streamsForAccount.Streams {
+ stream.Lock()
+ defer stream.Unlock()
+ if stream.Connected {
+ l.Debugf("streaming status to stream id %s", stream.ID)
+ stream.Messages <- >smodel.Message{
+ Stream: []string{stream.Type},
+ Event: "update",
+ Payload: string(statusBytes),
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/internal/processing/synchronous/status/boost.go b/internal/processing/synchronous/status/boost.go
deleted file mode 100644
index 93d0f19de..000000000
--- a/internal/processing/synchronous/status/boost.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package status
-
-import (
- "errors"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusBoost")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- if targetStatus.VisibilityAdvanced != nil {
- if !targetStatus.VisibilityAdvanced.Boostable {
- return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))
- }
- }
-
- // it's visible! it's boostable! so let's boost the FUCK out of it
- boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- boostWrapperStatus.CreatedWithApplicationID = application.ID
- boostWrapperStatus.GTSBoostedAccount = targetAccount
-
- // put the boost in the database
- if err := p.db.Put(boostWrapperStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsAnnounce,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: boostWrapperStatus,
- OriginAccount: account,
- TargetAccount: targetAccount,
- }
-
- // return the frontend representation of the new status to the submitter
- mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/boostedby.go b/internal/processing/synchronous/status/boostedby.go
deleted file mode 100644
index b352178e3..000000000
--- a/internal/processing/synchronous/status/boostedby.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package status
-
-import (
- "errors"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusBoostedBy")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible"))
- }
-
- // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
- favingAccounts, err := p.db.WhoBoostedStatus(targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))
- }
-
- // filter the list so the user doesn't see accounts they blocked or which blocked them
- filteredAccounts := []*gtsmodel.Account{}
- for _, acc := range favingAccounts {
- blocked, err := p.db.Blocked(account.ID, acc.ID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))
- }
- if !blocked {
- filteredAccounts = append(filteredAccounts, acc)
- }
- }
-
- // TODO: filter other things here? suspended? muted? silenced?
-
- // now we can return the masto representation of those accounts
- mastoAccounts := []*apimodel.Account{}
- for _, acc := range filteredAccounts {
- mastoAccount, err := p.tc.AccountToMastoPublic(acc)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
- }
- mastoAccounts = append(mastoAccounts, mastoAccount)
- }
-
- return mastoAccounts, nil
-}
diff --git a/internal/processing/synchronous/status/context.go b/internal/processing/synchronous/status/context.go
deleted file mode 100644
index 72b9b5623..000000000
--- a/internal/processing/synchronous/status/context.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package status
-
-import (
- "fmt"
- "sort"
-
- 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) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
-
- context := &apimodel.Context{
- Ancestors: []apimodel.Status{},
- Descendants: []apimodel.Status{},
- }
-
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(err)
- }
- if !visible {
- return nil, gtserror.NewErrorForbidden(fmt.Errorf("account with id %s does not have permission to view status %s", account.ID, targetStatusID))
- }
-
- parents, err := p.db.StatusParents(targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, status := range parents {
- if v, err := p.filter.StatusVisible(status, account); err == nil && v {
- mastoStatus, err := p.tc.StatusToMasto(status, account)
- if err == nil {
- context.Ancestors = append(context.Ancestors, *mastoStatus)
- }
- }
- }
-
- sort.Slice(context.Ancestors, func(i int, j int) bool {
- return context.Ancestors[i].ID < context.Ancestors[j].ID
- })
-
- children, err := p.db.StatusChildren(targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, status := range children {
- if v, err := p.filter.StatusVisible(status, account); err == nil && v {
- mastoStatus, err := p.tc.StatusToMasto(status, account)
- if err == nil {
- context.Descendants = append(context.Descendants, *mastoStatus)
- }
- }
- }
-
- return context, nil
-}
diff --git a/internal/processing/synchronous/status/create.go b/internal/processing/synchronous/status/create.go
deleted file mode 100644
index aa7468ae5..000000000
--- a/internal/processing/synchronous/status/create.go
+++ /dev/null
@@ -1,106 +0,0 @@
-package status
-
-import (
- "fmt"
- "time"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
- uris := util.GenerateURIsForAccount(account.Username, p.config.Protocol, p.config.Host)
- thisStatusID, err := id.NewULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
- thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
-
- newStatus := >smodel.Status{
- ID: thisStatusID,
- URI: thisStatusURI,
- URL: thisStatusURL,
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
- Local: true,
- AccountID: account.ID,
- AccountURI: account.URI,
- ContentWarning: form.SpoilerText,
- ActivityStreamsType: gtsmodel.ActivityStreamsNote,
- Sensitive: form.Sensitive,
- Language: form.Language,
- CreatedWithApplicationID: application.ID,
- Text: form.Status,
- }
-
- // check if replyToID is ok
- if err := p.processReplyToID(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // check if mediaIDs are ok
- if err := p.processMediaIDs(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // check if visibility settings are ok
- if err := p.processVisibility(form, account.Privacy, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // handle language settings
- if err := p.processLanguage(form, account.Language, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // handle mentions
- if err := p.processMentions(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if err := p.processTags(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if err := p.processEmojis(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if err := p.processContent(form, account.ID, newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // put the new status in the database, generating an ID for it in the process
- if err := p.db.Put(newStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // change the status ID of the media attachments to the new status
- for _, a := range newStatus.GTSMediaAttachments {
- a.StatusID = newStatus.ID
- a.UpdatedAt = time.Now()
- if err := p.db.UpdateByID(a.ID, a); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
- }
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: newStatus,
- OriginAccount: account,
- }
-
- // return the frontend representation of the new status to the submitter
- mastoStatus, err := p.tc.StatusToMasto(newStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/delete.go b/internal/processing/synchronous/status/delete.go
deleted file mode 100644
index 5da196a9f..000000000
--- a/internal/processing/synchronous/status/delete.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package status
-
-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) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusDelete")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- if _, ok := err.(db.ErrNoEntries); !ok {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- // status is already gone
- return nil, nil
- }
-
- if targetStatus.AccountID != account.ID {
- return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- if err := p.db.DeleteByID(targetStatus.ID, >smodel.Status{}); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
- }
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsDelete,
- GTSModel: targetStatus,
- OriginAccount: account,
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/fave.go b/internal/processing/synchronous/status/fave.go
deleted file mode 100644
index 23f0d2944..000000000
--- a/internal/processing/synchronous/status/fave.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package status
-
-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"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusFave")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
- }
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // is the status faveable?
- if targetStatus.VisibilityAdvanced != nil {
- if !targetStatus.VisibilityAdvanced.Likeable {
- return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
- }
- }
-
- // first check if the status is already faved, if so we don't need to do anything
- newFave := true
- gtsFave := >smodel.StatusFave{}
- if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil {
- // we already have a fave for this status
- newFave = false
- }
-
- if newFave {
- thisFaveID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // we need to create a new fave in the database
- gtsFave := >smodel.StatusFave{
- ID: thisFaveID,
- AccountID: account.ID,
- TargetAccountID: targetAccount.ID,
- StatusID: targetStatus.ID,
- URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID),
- GTSStatus: targetStatus,
- GTSTargetAccount: targetAccount,
- GTSFavingAccount: account,
- }
-
- if err := p.db.Put(gtsFave); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
- }
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsLike,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: gtsFave,
- OriginAccount: account,
- TargetAccount: targetAccount,
- }
- }
-
- // return the mastodon representation of the target status
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/favedby.go b/internal/processing/synchronous/status/favedby.go
deleted file mode 100644
index 5194cc258..000000000
--- a/internal/processing/synchronous/status/favedby.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package status
-
-import (
- "errors"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusFavedBy")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
- favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
- }
-
- // filter the list so the user doesn't see accounts they blocked or which blocked them
- filteredAccounts := []*gtsmodel.Account{}
- for _, acc := range favingAccounts {
- blocked, err := p.db.Blocked(account.ID, acc.ID)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
- }
- if !blocked {
- filteredAccounts = append(filteredAccounts, acc)
- }
- }
-
- // TODO: filter other things here? suspended? muted? silenced?
-
- // now we can return the masto representation of those accounts
- mastoAccounts := []*apimodel.Account{}
- for _, acc := range filteredAccounts {
- mastoAccount, err := p.tc.AccountToMastoPublic(acc)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
- mastoAccounts = append(mastoAccounts, mastoAccount)
- }
-
- return mastoAccounts, nil
-}
diff --git a/internal/processing/synchronous/status/get.go b/internal/processing/synchronous/status/get.go
deleted file mode 100644
index 9a70185b0..000000000
--- a/internal/processing/synchronous/status/get.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package status
-
-import (
- "errors"
- "fmt"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusGet")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- var boostOfStatus *gtsmodel.Status
- if targetStatus.BoostOfID != "" {
- boostOfStatus = >smodel.Status{}
- if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-
-}
diff --git a/internal/processing/synchronous/status/status.go b/internal/processing/synchronous/status/status.go
deleted file mode 100644
index d83c325fd..000000000
--- a/internal/processing/synchronous/status/status.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package status
-
-import (
- "github.com/sirupsen/logrus"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/visibility"
-)
-
-// Processor wraps a bunch of functions for processing statuses.
-type Processor interface {
- // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
- Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
- // Delete processes the delete of a given status, returning the deleted status if the delete goes through.
- Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Fave processes the faving of a given status, returning the updated status if the fave goes through.
- Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
- Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
- Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
- BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
- // FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
- FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
- // Get gets the given status, taking account of privacy settings and blocks etc.
- Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Unfave processes the unfaving of a given status, returning the updated status if the fave goes through.
- Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Context returns the context (previous and following posts) from the given status ID
- Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
-}
-
-type processor struct {
- tc typeutils.TypeConverter
- config *config.Config
- db db.DB
- filter visibility.Filter
- fromClientAPI chan gtsmodel.FromClientAPI
- log *logrus.Logger
-}
-
-// New returns a new status processor.
-func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan gtsmodel.FromClientAPI, log *logrus.Logger) Processor {
- return &processor{
- tc: tc,
- config: config,
- db: db,
- filter: visibility.NewFilter(db, log),
- fromClientAPI: fromClientAPI,
- log: log,
- }
-}
diff --git a/internal/processing/synchronous/status/unboost.go b/internal/processing/synchronous/status/unboost.go
deleted file mode 100644
index 2a1394695..000000000
--- a/internal/processing/synchronous/status/unboost.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package status
-
-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) Unboost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "Unboost")
-
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // check if we actually have a boost for this status
- var toUnboost bool
-
- gtsBoost := >smodel.Status{}
- where := []db.Where{
- {
- Key: "boost_of_id",
- Value: targetStatusID,
- },
- {
- Key: "account_id",
- Value: account.ID,
- },
- }
- err = p.db.GetWhere(where, gtsBoost)
- if err == nil {
- // we have a boost
- toUnboost = true
- }
-
- if err != nil {
- // something went wrong in the db finding the boost
- if _, ok := err.(db.ErrNoEntries); !ok {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
- }
- // we just don't have a boost
- toUnboost = false
- }
-
- if toUnboost {
- // we had a boost, so take some action to get rid of it
- if err := p.db.DeleteWhere(where, >smodel.Status{}); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unboosting status: %s", err))
- }
-
- // pin some stuff onto the boost while we have it out of the db
- gtsBoost.GTSBoostedStatus = targetStatus
- gtsBoost.GTSBoostedStatus.GTSAuthorAccount = targetAccount
- gtsBoost.GTSBoostedAccount = targetAccount
- gtsBoost.GTSAuthorAccount = account
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsAnnounce,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: gtsBoost,
- OriginAccount: account,
- TargetAccount: targetAccount,
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/unfave.go b/internal/processing/synchronous/status/unfave.go
deleted file mode 100644
index b51daacb9..000000000
--- a/internal/processing/synchronous/status/unfave.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package status
-
-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) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- l := p.log.WithField("func", "StatusUnfave")
- l.Tracef("going to search for target status %s", targetStatusID)
- targetStatus := >smodel.Status{}
- if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
-
- l.Tracef("going to search for target account %s", targetStatus.AccountID)
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
- }
-
- l.Trace("going to see if status is visible")
- visible, err := p.filter.StatusVisible(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
-
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // check if we actually have a fave for this status
- var toUnfave bool
-
- gtsFave := >smodel.StatusFave{}
- err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave)
- if err == nil {
- // we have a fave
- toUnfave = true
- }
- if err != nil {
- // something went wrong in the db finding the fave
- if _, ok := err.(db.ErrNoEntries); !ok {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
- }
- // we just don't have a fave
- toUnfave = false
- }
-
- if toUnfave {
- // we had a fave, so take some action to get rid of it
- if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
- }
-
- // send it back to the processor for async processing
- p.fromClientAPI <- gtsmodel.FromClientAPI{
- APObjectType: gtsmodel.ActivityStreamsLike,
- APActivityType: gtsmodel.ActivityStreamsUndo,
- GTSModel: gtsFave,
- OriginAccount: account,
- TargetAccount: targetAccount,
- }
- }
-
- mastoStatus, err := p.tc.StatusToMasto(targetStatus, account)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return mastoStatus, nil
-}
diff --git a/internal/processing/synchronous/status/util.go b/internal/processing/synchronous/status/util.go
deleted file mode 100644
index 0a023eab6..000000000
--- a/internal/processing/synchronous/status/util.go
+++ /dev/null
@@ -1,269 +0,0 @@
-package status
-
-import (
- "errors"
- "fmt"
- "strings"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
- "github.com/superseriousbusiness/gotosocial/internal/util"
-)
-
-func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
- // by default all flags are set to true
- gtsAdvancedVis := >smodel.VisibilityAdvanced{
- Federated: true,
- Boostable: true,
- Replyable: true,
- Likeable: true,
- }
-
- var gtsBasicVis gtsmodel.Visibility
- // Advanced takes priority if it's set.
- // If it's not set, take whatever masto visibility is set.
- // If *that's* not set either, then just take the account default.
- // If that's also not set, take the default for the whole instance.
- if form.VisibilityAdvanced != nil {
- gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
- } else if form.Visibility != "" {
- gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
- } else if accountDefaultVis != "" {
- gtsBasicVis = accountDefaultVis
- } else {
- gtsBasicVis = gtsmodel.VisibilityDefault
- }
-
- switch gtsBasicVis {
- case gtsmodel.VisibilityPublic:
- // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
- break
- case gtsmodel.VisibilityUnlocked:
- // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Boostable != nil {
- gtsAdvancedVis.Boostable = *form.Boostable
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
- // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
- gtsAdvancedVis.Boostable = false
-
- if form.Federated != nil {
- gtsAdvancedVis.Federated = *form.Federated
- }
-
- if form.Replyable != nil {
- gtsAdvancedVis.Replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- gtsAdvancedVis.Likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityDirect:
- // direct is pretty easy: there's only one possible setting so return it
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Boostable = false
- gtsAdvancedVis.Federated = true
- gtsAdvancedVis.Likeable = true
- }
-
- status.Visibility = gtsBasicVis
- status.VisibilityAdvanced = gtsAdvancedVis
- return nil
-}
-
-func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.InReplyToID == "" {
- return nil
- }
-
- // If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
- //
- // 1. Does the replied status exist in the database?
- // 2. Is the replied status marked as replyable?
- // 3. Does a block exist between either the current account or the account that posted the status it's replying to?
- //
- // If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
- repliedStatus := >smodel.Status{}
- repliedAccount := >smodel.Account{}
- // check replied status exists + is replyable
- if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
-
- if repliedStatus.VisibilityAdvanced != nil {
- if !repliedStatus.VisibilityAdvanced.Replyable {
- return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
- }
- }
-
- // check replied account is known to us
- if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
- if _, ok := err.(db.ErrNoEntries); ok {
- return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
- }
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- // check if a block exists
- if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
- if _, ok := err.(db.ErrNoEntries); !ok {
- return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
- }
- } else if blocked {
- return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
- }
- status.InReplyToID = repliedStatus.ID
- status.InReplyToAccountID = repliedAccount.ID
-
- return nil
-}
-
-func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
- if form.MediaIDs == nil {
- return nil
- }
-
- gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
- attachments := []string{}
- for _, mediaID := range form.MediaIDs {
- // check these attachments exist
- a := >smodel.MediaAttachment{}
- if err := p.db.GetByID(mediaID, a); err != nil {
- return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
- }
- // check they belong to the requesting account id
- if a.AccountID != thisAccountID {
- return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
- }
- // check they're not already used in a status
- if a.StatusID != "" || a.ScheduledStatusID != "" {
- return fmt.Errorf("media with id %s is already attached to a status", mediaID)
- }
- gtsMediaAttachments = append(gtsMediaAttachments, a)
- attachments = append(attachments, a.ID)
- }
- status.GTSMediaAttachments = gtsMediaAttachments
- status.Attachments = attachments
- return nil
-}
-
-func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
- if form.Language != "" {
- status.Language = form.Language
- } else {
- status.Language = accountDefaultLanguage
- }
- if status.Language == "" {
- return errors.New("no language given either in status create form or account default")
- }
- return nil
-}
-
-func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- menchies := []string{}
- gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating mentions from status: %s", err)
- }
- for _, menchie := range gtsMenchies {
- menchieID, err := id.NewRandomULID()
- if err != nil {
- return err
- }
- menchie.ID = menchieID
-
- if err := p.db.Put(menchie); err != nil {
- return fmt.Errorf("error putting mentions in db: %s", err)
- }
- menchies = append(menchies, menchie.ID)
- }
- // add full populated gts menchies to the status for passing them around conveniently
- status.GTSMentions = gtsMenchies
- // add just the ids of the mentioned accounts to the status for putting in the db
- status.Mentions = menchies
- return nil
-}
-
-func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- tags := []string{}
- gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating hashtags from status: %s", err)
- }
- for _, tag := range gtsTags {
- if err := p.db.Upsert(tag, "name"); err != nil {
- return fmt.Errorf("error putting tags in db: %s", err)
- }
- tags = append(tags, tag.ID)
- }
- // add full populated gts tags to the status for passing them around conveniently
- status.GTSTags = gtsTags
- // add just the ids of the used tags to the status for putting in the db
- status.Tags = tags
- return nil
-}
-
-func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- emojis := []string{}
- gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
- if err != nil {
- return fmt.Errorf("error generating emojis from status: %s", err)
- }
- for _, e := range gtsEmojis {
- emojis = append(emojis, e.ID)
- }
- // add full populated gts emojis to the status for passing them around conveniently
- status.GTSEmojis = gtsEmojis
- // add just the ids of the used emojis to the status for putting in the db
- status.Emojis = emojis
- return nil
-}
-
-func (p *processor) processContent(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- if form.Status == "" {
- status.Content = ""
- return nil
- }
-
- // surround the whole status in ''
- content := fmt.Sprintf(`
%s
`, form.Status)
-
- // format mentions nicely
- for _, menchie := range status.GTSMentions {
- targetAccount := >smodel.Account{}
- if err := p.db.GetByID(menchie.TargetAccountID, targetAccount); err == nil {
- mentionContent := fmt.Sprintf(`@%s`, targetAccount.URL, targetAccount.Username)
- content = strings.ReplaceAll(content, menchie.NameString, mentionContent)
- }
- }
-
- // format tags nicely
- for _, tag := range status.GTSTags {
- tagContent := fmt.Sprintf(`#%s`, tag.URL, tag.Name)
- content = strings.ReplaceAll(content, fmt.Sprintf("#%s", tag.Name), tagContent)
- }
-
- // replace newlines with breaks
- content = strings.ReplaceAll(content, "\n", "
")
-
- status.Content = content
- return nil
-}
diff --git a/internal/processing/synchronous/streaming/authorize.go b/internal/processing/synchronous/streaming/authorize.go
deleted file mode 100644
index 8bbf1856d..000000000
--- a/internal/processing/synchronous/streaming/authorize.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package streaming
-
-import (
- "context"
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error) {
- ti, err := p.oauthServer.LoadAccessToken(context.Background(), accessToken)
- if err != nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: error loading access token: %s", err)
- }
-
- uid := ti.GetUserID()
- if uid == "" {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no userid in token")
- }
-
- // fetch user's and account for this user id
- user := >smodel.User{}
- if err := p.db.GetByID(uid, user); err != nil || user == nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no user found for validated uid %s", uid)
- }
-
- acct := >smodel.Account{}
- if err := p.db.GetByID(user.AccountID, acct); err != nil || acct == nil {
- return nil, fmt.Errorf("AuthorizeStreamingRequest: no account retrieved for user with id %s", uid)
- }
-
- return acct, nil
-}
diff --git a/internal/processing/synchronous/streaming/openstream.go b/internal/processing/synchronous/streaming/openstream.go
deleted file mode 100644
index 68446bac6..000000000
--- a/internal/processing/synchronous/streaming/openstream.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package streaming
-
-import (
- "errors"
- "fmt"
-
- "github.com/sirupsen/logrus"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/id"
-)
-
-func (p *processor) OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode) {
- l := p.log.WithFields(logrus.Fields{
- "func": "OpenStreamForAccount",
- "account": account.ID,
- "streamType": streamType,
- })
- l.Debug("received open stream request")
-
- // each stream needs a unique ID so we know to close it
- streamID, err := id.NewRandomULID()
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error generating stream id: %s", err))
- }
-
- thisStream := >smodel.Stream{
- ID: streamID,
- Type: streamType,
- Messages: make(chan *gtsmodel.Message, 100),
- Hangup: make(chan interface{}, 1),
- Connected: true,
- }
- go p.waitToCloseStream(account, thisStream)
-
- v, ok := p.streamMap.Load(account.ID)
- if !ok || v == nil {
- // there is no entry in the streamMap for this account yet, so make one and store it
- streamsForAccount := >smodel.StreamsForAccount{
- Streams: []*gtsmodel.Stream{
- thisStream,
- },
- }
- p.streamMap.Store(account.ID, streamsForAccount)
- } else {
- // there is an entry in the streamMap for this account
- // parse the interface as a streamsForAccount
- streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
- if !ok {
- return nil, gtserror.NewErrorInternalError(errors.New("stream map error"))
- }
-
- // append this stream to it
- streamsForAccount.Lock()
- streamsForAccount.Streams = append(streamsForAccount.Streams, thisStream)
- streamsForAccount.Unlock()
- }
-
- return thisStream, nil
-}
-
-// waitToCloseStream waits until the hangup channel is closed for the given stream.
-// It then iterates through the map of streams stored by the processor, removes the stream from it,
-// and then closes the messages channel of the stream to indicate that the channel should no longer be read from.
-func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *gtsmodel.Stream) {
- <-thisStream.Hangup // wait for a hangup message
-
- // lock the stream to prevent more messages being put in it while we work
- thisStream.Lock()
- defer thisStream.Unlock()
-
- // indicate the stream is no longer connected
- thisStream.Connected = false
-
- // load and parse the entry for this account from the stream map
- v, ok := p.streamMap.Load(account.ID)
- if !ok || v == nil {
- return
- }
- streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
- if !ok {
- return
- }
-
- // lock the streams for account while we remove this stream from its slice
- streamsForAccount.Lock()
- defer streamsForAccount.Unlock()
-
- // put everything into modified streams *except* the stream we're removing
- modifiedStreams := []*gtsmodel.Stream{}
- for _, s := range streamsForAccount.Streams {
- if s.ID != thisStream.ID {
- modifiedStreams = append(modifiedStreams, s)
- }
- }
- streamsForAccount.Streams = modifiedStreams
-
- // finally close the messages channel so no more messages can be read from it
- close(thisStream.Messages)
-}
diff --git a/internal/processing/synchronous/streaming/streamdelete.go b/internal/processing/synchronous/streaming/streamdelete.go
deleted file mode 100644
index 2282c29ae..000000000
--- a/internal/processing/synchronous/streaming/streamdelete.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package streaming
-
-import (
- "fmt"
- "strings"
-
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) StreamDelete(statusID string) error {
- errs := []string{}
-
- // we want to range through ALL streams for ALL accounts here to make sure it's very clear to everyone that the status has been deleted
- p.streamMap.Range(func(k interface{}, v interface{}) bool {
- // the key of this map should be an accountID (string)
- accountID, ok := k.(string)
- if !ok {
- errs = append(errs, "key in streamMap was not a string!")
- return false
- }
-
- // the value of the map should be a buncha streams
- streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
- if !ok {
- errs = append(errs, fmt.Sprintf("stream map error for account stream %s", accountID))
- }
-
- // lock the streams while we work on them
- streamsForAccount.Lock()
- defer streamsForAccount.Unlock()
- for _, stream := range streamsForAccount.Streams {
- // lock each individual stream as we work on it
- stream.Lock()
- defer stream.Unlock()
- if stream.Connected {
- stream.Messages <- >smodel.Message{
- Stream: []string{stream.Type},
- Event: "delete",
- Payload: statusID,
- }
- }
- }
- return true
- })
-
- if len(errs) != 0 {
- return fmt.Errorf("one or more errors streaming status delete: %s", strings.Join(errs, ";"))
- }
-
- return nil
-}
diff --git a/internal/processing/synchronous/streaming/streaming.go b/internal/processing/synchronous/streaming/streaming.go
deleted file mode 100644
index de75b8f27..000000000
--- a/internal/processing/synchronous/streaming/streaming.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package streaming
-
-import (
- "sync"
-
- "github.com/sirupsen/logrus"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "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/oauth"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
- "github.com/superseriousbusiness/gotosocial/internal/visibility"
-)
-
-// Processor wraps a bunch of functions for processing streaming.
-type Processor interface {
- // AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API
- AuthorizeStreamingRequest(accessToken string) (*gtsmodel.Account, error)
- // OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller.
- OpenStreamForAccount(account *gtsmodel.Account, streamType string) (*gtsmodel.Stream, gtserror.WithCode)
- // StreamStatusToAccount streams the given status to any open, appropriate streams belonging to the given account.
- StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error
- // StreamNotificationToAccount streams the given notification to any open, appropriate streams belonging to the given account.
- StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error
- // StreamDelete streams the delete of the given statusID to *ALL* open streams.
- StreamDelete(statusID string) error
-}
-
-type processor struct {
- tc typeutils.TypeConverter
- config *config.Config
- db db.DB
- filter visibility.Filter
- log *logrus.Logger
- oauthServer oauth.Server
- streamMap *sync.Map
-}
-
-// New returns a new status processor.
-func New(db db.DB, tc typeutils.TypeConverter, oauthServer oauth.Server, config *config.Config, log *logrus.Logger) Processor {
- return &processor{
- tc: tc,
- config: config,
- db: db,
- filter: visibility.NewFilter(db, log),
- log: log,
- oauthServer: oauthServer,
- streamMap: &sync.Map{},
- }
-}
diff --git a/internal/processing/synchronous/streaming/streamnotification.go b/internal/processing/synchronous/streaming/streamnotification.go
deleted file mode 100644
index 24c8342ee..000000000
--- a/internal/processing/synchronous/streaming/streamnotification.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package streaming
-
-import (
- "encoding/json"
- "errors"
- "fmt"
-
- "github.com/sirupsen/logrus"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "StreamNotificationToAccount",
- "account": account.ID,
- })
- v, ok := p.streamMap.Load(account.ID)
- if !ok {
- // no open connections so nothing to stream
- return nil
- }
-
- streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
- if !ok {
- return errors.New("stream map error")
- }
-
- notificationBytes, err := json.Marshal(n)
- if err != nil {
- return fmt.Errorf("error marshalling notification to json: %s", err)
- }
-
- streamsForAccount.Lock()
- defer streamsForAccount.Unlock()
- for _, stream := range streamsForAccount.Streams {
- stream.Lock()
- defer stream.Unlock()
- if stream.Connected {
- l.Debugf("streaming notification to stream id %s", stream.ID)
- stream.Messages <- >smodel.Message{
- Stream: []string{stream.Type},
- Event: "notification",
- Payload: string(notificationBytes),
- }
- }
- }
-
- return nil
-}
diff --git a/internal/processing/synchronous/streaming/streamstatus.go b/internal/processing/synchronous/streaming/streamstatus.go
deleted file mode 100644
index 8d026252d..000000000
--- a/internal/processing/synchronous/streaming/streamstatus.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package streaming
-
-import (
- "encoding/json"
- "errors"
- "fmt"
-
- "github.com/sirupsen/logrus"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) StreamStatusToAccount(s *apimodel.Status, account *gtsmodel.Account) error {
- l := p.log.WithFields(logrus.Fields{
- "func": "StreamStatusForAccount",
- "account": account.ID,
- })
- v, ok := p.streamMap.Load(account.ID)
- if !ok {
- // no open connections so nothing to stream
- return nil
- }
-
- streamsForAccount, ok := v.(*gtsmodel.StreamsForAccount)
- if !ok {
- return errors.New("stream map error")
- }
-
- statusBytes, err := json.Marshal(s)
- if err != nil {
- return fmt.Errorf("error marshalling status to json: %s", err)
- }
-
- streamsForAccount.Lock()
- defer streamsForAccount.Unlock()
- for _, stream := range streamsForAccount.Streams {
- stream.Lock()
- defer stream.Unlock()
- if stream.Connected {
- l.Debugf("streaming status to stream id %s", stream.ID)
- stream.Messages <- >smodel.Message{
- Stream: []string{stream.Type},
- Event: "update",
- Payload: string(statusBytes),
- }
- }
- }
-
- return nil
-}
diff --git a/internal/processing/util.go b/internal/processing/util.go
deleted file mode 100644
index 6474100c1..000000000
--- a/internal/processing/util.go
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- 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 processing
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "mime/multipart"
-
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/transport"
-)
-
-/*
- HELPER FUNCTIONS
-*/
-
-// TODO: try to combine the below two functions because this is a lot of code repetition.
-
-// updateAccountAvatar does the dirty work of checking the avatar part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new avatar image.
-func (p *processor) updateAccountAvatar(avatar *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(avatar.Size) > p.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("avatar with size %d exceeded max image size of %d bytes", avatar.Size, p.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := avatar.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided avatar: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided avatar: size 0 bytes")
- }
-
- // do the setting
- avatarInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Avatar, "")
- if err != nil {
- return nil, fmt.Errorf("error processing avatar: %s", err)
- }
-
- return avatarInfo, f.Close()
-}
-
-// updateAccountHeader does the dirty work of checking the header part of an account update form,
-// parsing and checking the image, and doing the necessary updates in the database for this to become
-// the account's new header image.
-func (p *processor) updateAccountHeader(header *multipart.FileHeader, accountID string) (*gtsmodel.MediaAttachment, error) {
- var err error
- if int(header.Size) > p.config.MediaConfig.MaxImageSize {
- err = fmt.Errorf("header with size %d exceeded max image size of %d bytes", header.Size, p.config.MediaConfig.MaxImageSize)
- return nil, err
- }
- f, err := header.Open()
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
-
- // extract the bytes
- buf := new(bytes.Buffer)
- size, err := io.Copy(buf, f)
- if err != nil {
- return nil, fmt.Errorf("could not read provided header: %s", err)
- }
- if size == 0 {
- return nil, errors.New("could not read provided header: size 0 bytes")
- }
-
- // do the setting
- headerInfo, err := p.mediaHandler.ProcessHeaderOrAvatar(buf.Bytes(), accountID, media.Header, "")
- if err != nil {
- return nil, fmt.Errorf("error processing header: %s", err)
- }
-
- return headerInfo, f.Close()
-}
-
-// fetchHeaderAndAviForAccount fetches the header and avatar for a remote account, using a transport
-// on behalf of requestingUsername.
-//
-// targetAccount's AvatarMediaAttachmentID and HeaderMediaAttachmentID will be updated as necessary.
-//
-// SIDE EFFECTS: remote header and avatar will be stored in local storage, and the database will be updated
-// to reflect the creation of these new attachments.
-func (p *processor) fetchHeaderAndAviForAccount(targetAccount *gtsmodel.Account, t transport.Transport, refresh bool) error {
- if targetAccount.AvatarRemoteURL != "" && (targetAccount.AvatarMediaAttachmentID == "" || refresh) {
- a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
- RemoteURL: targetAccount.AvatarRemoteURL,
- Avatar: true,
- }, targetAccount.ID)
- if err != nil {
- return fmt.Errorf("error processing avatar for user: %s", err)
- }
- targetAccount.AvatarMediaAttachmentID = a.ID
- }
-
- if targetAccount.HeaderRemoteURL != "" && (targetAccount.HeaderMediaAttachmentID == "" || refresh) {
- a, err := p.mediaHandler.ProcessRemoteHeaderOrAvatar(t, >smodel.MediaAttachment{
- RemoteURL: targetAccount.HeaderRemoteURL,
- Header: true,
- }, targetAccount.ID)
- if err != nil {
- return fmt.Errorf("error processing header for user: %s", err)
- }
- targetAccount.HeaderMediaAttachmentID = a.ID
- }
- return nil
-}
--
cgit v1.2.3