From 1f44b06c06c7c38a1c10247989cb8b4f34b3c98e Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 4 Jun 2021 14:16:29 +0200 Subject: [PATCH] restructure a bunch, get unfaves working --- internal/api/s2s/user/inboxpost.go | 4 +- .../error.go => gtserror/withcode.go} | 34 +- internal/processing/account.go | 85 +-- internal/processing/federation.go | 67 +-- internal/processing/followrequest.go | 23 +- internal/processing/fromclientapi.go | 46 ++ internal/processing/fromcommon.go | 4 + internal/processing/instance.go | 7 +- internal/processing/media.go | 55 +- internal/processing/notification.go | 5 +- internal/processing/processor.go | 73 +-- internal/processing/search.go | 5 +- internal/processing/status.go | 516 +----------------- internal/processing/status/boost.go | 79 +++ internal/processing/status/boostedby.go | 74 +++ internal/processing/status/context.go | 11 + internal/processing/status/create.go | 98 ++++ internal/processing/status/delete.go | 55 ++ internal/processing/status/fave.go | 104 ++++ internal/processing/status/favedby.go | 74 +++ internal/processing/status/get.go | 58 ++ internal/processing/status/status.go | 52 ++ internal/processing/status/unfave.go | 93 ++++ internal/processing/status/util.go | 230 ++++++++ internal/processing/timeline.go | 19 +- 25 files changed, 1190 insertions(+), 681 deletions(-) rename internal/{processing/error.go => gtserror/withcode.go} (80%) 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/unfave.go create mode 100644 internal/processing/status/util.go diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go index 642ba64..3e7d3d6 100644 --- a/internal/api/s2s/user/inboxpost.go +++ b/internal/api/s2s/user/inboxpost.go @@ -23,7 +23,7 @@ import ( "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/processing" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. @@ -42,7 +42,7 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request) if err != nil { - if withCode, ok := err.(processing.ErrorWithCode); ok { + if withCode, ok := err.(gtserror.WithCode); ok { l.Debug(withCode.Error()) c.JSON(withCode.Code(), withCode.Safe()) return diff --git a/internal/processing/error.go b/internal/gtserror/withcode.go similarity index 80% rename from internal/processing/error.go rename to internal/gtserror/withcode.go index 1fea01d..cb05b2a 100644 --- a/internal/processing/error.go +++ b/internal/gtserror/withcode.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package processing +package gtserror import ( "errors" @@ -24,12 +24,12 @@ import ( "strings" ) -// ErrorWithCode wraps an internal error with an http code, and a 'safe' version of +// WithCode wraps an internal error with an http code, and a 'safe' version of // the error that can be served to clients without revealing internal business logic. // // A typical use of this error would be to first log the Original error, then return // the Safe error and the StatusCode to an API caller. -type ErrorWithCode interface { +type WithCode interface { // Error returns the original internal error for debugging within the GoToSocial logs. // This should *NEVER* be returned to a client as it may contain sensitive information. Error() string @@ -40,31 +40,31 @@ type ErrorWithCode interface { Code() int } -type errorWithCode struct { +type withCode struct { original error safe error code int } -func (e errorWithCode) Error() string { +func (e withCode) Error() string { return e.original.Error() } -func (e errorWithCode) Safe() string { +func (e withCode) Safe() string { return e.safe.Error() } -func (e errorWithCode) Code() int { +func (e withCode) Code() int { return e.code } // NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text. -func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode { +func NewErrorBadRequest(original error, helpText ...string) WithCode { safe := "bad request" if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } - return errorWithCode{ + return withCode{ original: original, safe: errors.New(safe), code: http.StatusBadRequest, @@ -72,12 +72,12 @@ func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode { } // NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text. -func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode { +func NewErrorNotAuthorized(original error, helpText ...string) WithCode { safe := "not authorized" if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } - return errorWithCode{ + return withCode{ original: original, safe: errors.New(safe), code: http.StatusUnauthorized, @@ -85,12 +85,12 @@ func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode { } // NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text. -func NewErrorForbidden(original error, helpText ...string) ErrorWithCode { +func NewErrorForbidden(original error, helpText ...string) WithCode { safe := "forbidden" if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } - return errorWithCode{ + return withCode{ original: original, safe: errors.New(safe), code: http.StatusForbidden, @@ -98,12 +98,12 @@ func NewErrorForbidden(original error, helpText ...string) ErrorWithCode { } // NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text. -func NewErrorNotFound(original error, helpText ...string) ErrorWithCode { +func NewErrorNotFound(original error, helpText ...string) WithCode { safe := "404 not found" if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } - return errorWithCode{ + return withCode{ original: original, safe: errors.New(safe), code: http.StatusNotFound, @@ -111,12 +111,12 @@ func NewErrorNotFound(original error, helpText ...string) ErrorWithCode { } // NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text. -func NewErrorInternalError(original error, helpText ...string) ErrorWithCode { +func NewErrorInternalError(original error, helpText ...string) WithCode { safe := "internal server error" if helpText != nil { safe = safe + ": " + strings.Join(helpText, ": ") } - return errorWithCode{ + return withCode{ original: original, safe: errors.New(safe), code: http.StatusInternalServerError, diff --git a/internal/processing/account.go b/internal/processing/account.go index 71faa74..28fbb51 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -25,6 +25,7 @@ import ( "github.com/google/uuid" 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/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -202,13 +203,13 @@ func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCrede return acctSensitive, nil } -func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) { +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, NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID)) } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } statuses := []gtsmodel.Status{} @@ -217,18 +218,18 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin if _, ok := err.(db.ErrNoEntries); ok { return apiStatuses, nil } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } for _, s := range statuses { relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err)) } visible, err := p.db.StatusVisible(&s, targetAccount, authed.Account, relevantAccounts) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err)) } if !visible { continue @@ -238,16 +239,16 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin if s.BoostOfID != "" { bs := >smodel.Status{} if err := p.db.GetByID(s.BoostOfID, bs); err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err)) } boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err)) } boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err)) } if boostedVisible { @@ -257,7 +258,7 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err)) } apiStatuses = append(apiStatuses, *apiStatus) @@ -266,14 +267,14 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin return apiStatuses, nil } -func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) { +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, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts")) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } followers := []gtsmodel.Follow{} @@ -282,13 +283,13 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri if _, ok := err.(db.ErrNoEntries); ok { return accounts, nil } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } for _, f := range followers { blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { continue @@ -299,7 +300,7 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri if _, ok := err.(db.ErrNoEntries); ok { continue } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } // derefence account fields in case we haven't done it already @@ -310,21 +311,21 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri account, err := p.tc.AccountToMastoPublic(a) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } accounts = append(accounts, *account) } return accounts, nil } -func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) { +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, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts")) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) } following := []gtsmodel.Follow{} @@ -333,13 +334,13 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri if _, ok := err.(db.ErrNoEntries); ok { return accounts, nil } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } for _, f := range following { blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { continue @@ -350,7 +351,7 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri if _, ok := err.(db.ErrNoEntries); ok { continue } - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } // derefence account fields in case we haven't done it already @@ -361,53 +362,53 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri account, err := p.tc.AccountToMastoPublic(a) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } accounts = append(accounts, *account) } return accounts, nil } -func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { +func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { if authed == nil || authed.Account == nil { - return nil, NewErrorForbidden(errors.New("not authed")) + return nil, gtserror.NewErrorForbidden(errors.New("not authed")) } gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) } r, err := p.tc.RelationshipToMasto(gtsR) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) } return r, nil } -func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) { +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, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts")) + 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, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err)) + 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, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) } if follows { // already follows so just return the relationship @@ -417,7 +418,7 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou // check if a follow exists already followRequested, err := p.db.FollowRequested(authed.Account, targetAcct) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) + 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 @@ -445,13 +446,13 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou // whack it in the database if err := p.db.Put(fr); err != nil { - return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err)) + 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, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err)) + 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) @@ -470,21 +471,21 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou return p.AccountRelationshipGet(authed, form.TargetAccountID) } -func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) { +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, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) + 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, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) } } @@ -498,7 +499,7 @@ func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID stri }, fr); err == nil { frURI = fr.URI if err := p.db.DeleteByID(fr.ID, fr); err != nil { - return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) } frChanged = true } @@ -513,7 +514,7 @@ func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID stri }, f); err == nil { fURI = f.URI if err := p.db.DeleteByID(f.ID, f); err != nil { - return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) } fChanged = true } diff --git a/internal/processing/federation.go b/internal/processing/federation.go index b93455d..dacaa25 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -27,6 +27,7 @@ import ( "github.com/go-fed/activity/streams" 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/util" ) @@ -88,141 +89,141 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht return requestingAccount, nil } -func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { +func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } // authenticate the request requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) if err != nil { - return nil, NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorNotAuthorized(err) } blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } requestedPerson, err := p.tc.AccountToAS(requestedAccount) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } data, err := streams.Serialize(requestedPerson) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return data, nil } -func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { +func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } // authenticate the request requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) if err != nil { - return nil, NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorNotAuthorized(err) } blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } requestedAccountURI, err := url.Parse(requestedAccount.URI) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) } requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) } data, err := streams.Serialize(requestedFollowers) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return data, nil } -func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) { +func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } // authenticate the request requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) if err != nil { - return nil, NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorNotAuthorized(err) } blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } requestedAccountURI, err := url.Parse(requestedAccount.URI) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) } requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) } data, err := streams.Serialize(requestedFollowing) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return data, nil } -func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) { +func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } // authenticate the request requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request) if err != nil { - return nil, NewErrorNotAuthorized(err) + return nil, gtserror.NewErrorNotAuthorized(err) } blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } if blocked { - return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) } s := >smodel.Status{} @@ -230,27 +231,27 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st {Key: "id", Value: requestedStatusID}, {Key: "account_id", Value: requestedAccount.ID}, }, s); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) } asStatus, err := p.tc.StatusToAS(s) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } data, err := streams.Serialize(asStatus) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return data, nil } -func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) { +func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode) { // get the account the request is referring to requestedAccount := >smodel.Account{} if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) } // return the webfinger representation diff --git a/internal/processing/followrequest.go b/internal/processing/followrequest.go index 7e606f5..5eb9fd6 100644 --- a/internal/processing/followrequest.go +++ b/internal/processing/followrequest.go @@ -21,15 +21,16 @@ package processing 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" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) { +func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { frs := []gtsmodel.FollowRequest{} if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil { if _, ok := err.(db.ErrNoEntries); !ok { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } } @@ -37,31 +38,31 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err for _, fr := range frs { acct := >smodel.Account{} if err := p.db.GetByID(fr.AccountID, acct); err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } mastoAcct, err := p.tc.AccountToMastoPublic(acct) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } accts = append(accts, *mastoAcct) } return accts, nil } -func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) { +func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID) if err != nil { - return nil, NewErrorNotFound(err) + return nil, gtserror.NewErrorNotFound(err) } originAccount := >smodel.Account{} if err := p.db.GetByID(follow.AccountID, originAccount); err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } targetAccount := >smodel.Account{} if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } p.fromClientAPI <- gtsmodel.FromClientAPI{ @@ -74,17 +75,17 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } r, err := p.tc.RelationshipToMasto(gtsR) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return r, nil } -func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode { +func (p *processor) FollowRequestDeny(auth *oauth.Auth) gtserror.WithCode { return nil } diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index e7fe28c..7f75982 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -127,6 +127,13 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error return errors.New("undo was not parseable as *gtsmodel.Follow") } return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount) + case gtsmodel.ActivityStreamsLike: + // UNDO LIKE/FAVE + fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) + if !ok { + return errors.New("undo was not parseable as *gtsmodel.StatusFave") + } + return p.federateUnfave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount) } } return nil @@ -210,6 +217,45 @@ func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gts return err } +func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { + // if both accounts are local there's nothing to do here + if originAccount.Domain == "" && targetAccount.Domain == "" { + return nil + } + + // create the AS fave + asFave, err := p.tc.FaveToAS(fave) + if err != nil { + return fmt.Errorf("federateFave: error converting fave to as format: %s", err) + } + + targetAccountURI, err := url.Parse(targetAccount.URI) + if err != nil { + return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err) + } + + // create an Undo and set the appropriate actor on it + undo := streams.NewActivityStreamsUndo() + undo.SetActivityStreamsActor(asFave.GetActivityStreamsActor()) + + // Set the fave as the 'object' property. + undoObject := streams.NewActivityStreamsObjectProperty() + undoObject.AppendActivityStreamsLike(asFave) + undo.SetActivityStreamsObject(undoObject) + + // Set the To of the undo as the target of the fave + undoTo := streams.NewActivityStreamsToProperty() + undoTo.AppendIRI(targetAccountURI) + undo.SetActivityStreamsTo(undoTo) + + outboxIRI, err := url.Parse(originAccount.OutboxURI) + if err != nil { + return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err) + } + _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo) + return err +} + func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { // if both accounts are local there's nothing to do here if originAccount.Domain == "" && targetAccount.Domain == "" { diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index 5abb767..3294e9a 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -300,3 +300,7 @@ func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID errors <- fmt.Errorf("initTimelineFor: error ingesting status %s: %s", status.ID, err) } } + +func (p *processor) fullyDeleteStatus(status *gtsmodel.Status, accountID string) error { + return nil +} diff --git a/internal/processing/instance.go b/internal/processing/instance.go index e928bf6..9381a73 100644 --- a/internal/processing/instance.go +++ b/internal/processing/instance.go @@ -23,18 +23,19 @@ 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) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) { +func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) { i := >smodel.Instance{} if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil { - return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err)) } ai, err := p.tc.InstanceToMasto(i) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err)) } return ai, nil diff --git a/internal/processing/media.go b/internal/processing/media.go index 255f490..4f15632 100644 --- a/internal/processing/media.go +++ b/internal/processing/media.go @@ -28,6 +28,7 @@ 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" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -92,64 +93,64 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq return &mastoAttachment, nil } -func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, ErrorWithCode) { +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, NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) } - return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) } if attachment.AccountID != authed.Account.ID { - return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account")) + return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account")) } a, err := p.tc.AttachmentToMasto(attachment) if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) } return &a, nil } -func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) { +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, NewErrorNotFound(errors.New("attachment doesn't exist in the db")) + return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db")) } - return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err)) } if attachment.AccountID != authed.Account.ID { - return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account")) + 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, NewErrorInternalError(fmt.Errorf("database error updating description: %s", err)) + 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, NewErrorBadRequest(err) + 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, NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err)) } } a, err := p.tc.AttachmentToMasto(attachment) if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err)) } return &a, nil @@ -159,37 +160,37 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest // parse the form fields mediaSize, err := media.ParseMediaSize(form.MediaSize) if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) } mediaType, err := media.ParseMediaType(form.MediaType) if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) + 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, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName)) + 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, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err)) + 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, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID)) + 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, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err)) + 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, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID)) } } @@ -201,10 +202,10 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest case media.Emoji: e := >smodel.Emoji{} if err := p.db.GetByID(wantedMediaID, e); err != nil { - return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err)) } if e.Disabled { - return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID)) } switch mediaSize { case media.Original: @@ -214,15 +215,15 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest content.ContentType = e.ImageStaticContentType storagePath = e.ImageStaticPath default: - return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize)) + 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, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) + 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, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID)) } switch mediaSize { case media.Original: @@ -232,13 +233,13 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest content.ContentType = a.Thumbnail.ContentType storagePath = a.Thumbnail.Path default: - return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize)) + 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, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err)) } content.ContentLength = int64(len(bytes)) diff --git a/internal/processing/notification.go b/internal/processing/notification.go index 44e3885..6ad9741 100644 --- a/internal/processing/notification.go +++ b/internal/processing/notification.go @@ -20,15 +20,16 @@ package processing import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, ErrorWithCode) { +func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) { l := p.log.WithField("func", "NotificationsGet") notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID, sinceID) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } mastoNotifs := []*apimodel.Notification{} diff --git a/internal/processing/processor.go b/internal/processing/processor.go index e4d860f..65ecfa5 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -28,9 +28,11 @@ import ( "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/processing/status" "github.com/superseriousbusiness/gotosocial/internal/processing/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) @@ -71,17 +73,17 @@ type Processor interface { AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error) // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for // the account given in authed. - AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) + AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) // AccountFollowersGet fetches a list of the target account's followers. - AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) + AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // AccountFollowingGet fetches a list of the accounts that target account is following. - AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) + AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) // AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. - AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) + AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) // AccountFollowCreate handles a follow request to an account, either remote or local. - AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) + AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local. - AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) + AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) // 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) @@ -93,25 +95,25 @@ type Processor interface { FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error) // FollowRequestsGet handles the getting of the authed account's incoming follow requests - FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) + FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) // FollowRequestAccept handles the acceptance of a follow request from the given account ID - FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) + FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) // InstanceGet retrieves instance information for serving at api/v1/instance - InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) + InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) // MediaCreate handles the creation of a media attachment, using the given form. MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error) // MediaGet handles the GET of a media attachment with the given ID - MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, ErrorWithCode) + MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode) // MediaUpdate handles the PUT of a media attachment with the given ID and form - MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) + MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) // NotificationsGet - NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, ErrorWithCode) + NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) // SearchGet performs a search with the given params, resolving/dereferencing remotely as desired - SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode) + SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) @@ -120,9 +122,9 @@ type Processor interface { // StatusFave processes the faving of a given status, returning the updated status if the fave goes through. StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) // StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well. - StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode) + StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. - StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, ErrorWithCode) + StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) // StatusGet gets the given status, taking account of privacy settings and blocks etc. @@ -130,12 +132,12 @@ type Processor interface { // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through. StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) // StatusGetContext returns the context (previous and following posts) from the given status ID - StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, ErrorWithCode) + StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters. - HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, ErrorWithCode) + HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) // PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters. - PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, ErrorWithCode) + PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) /* FEDERATION API-FACING PROCESSING FUNCTIONS @@ -147,22 +149,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{}, ErrorWithCode) + GetFediUser(requestedUsername string, request *http.Request) (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{}, ErrorWithCode) + GetFediFollowers(requestedUsername string, request *http.Request) (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{}, ErrorWithCode) + GetFediFollowing(requestedUsername string, request *http.Request) (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{}, ErrorWithCode) + GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (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.WebfingerAccountResponse, ErrorWithCode) + GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode) // InboxPost handles POST requests to a user's inbox for new activitypub messages. // @@ -179,10 +181,7 @@ type Processor interface { // processor just implements the Processor interface type processor struct { - // federator pub.FederatingActor - // toClientAPI chan gtsmodel.ToClientAPI - fromClientAPI chan gtsmodel.FromClientAPI - // toFederator chan gtsmodel.ToFederator + fromClientAPI chan gtsmodel.FromClientAPI fromFederator chan gtsmodel.FromFederator federator federation.Federator stop chan interface{} @@ -194,15 +193,25 @@ type processor struct { storage blob.Storage timelineManager timeline.Manager db db.DB + + /* + SUB-PROCESSORS + */ + + statusProcessor status.Processor } // NewProcessor returns a new Processor that uses the given federator and logger func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage blob.Storage, timelineManager timeline.Manager, db db.DB, log *logrus.Logger) Processor { + + fromClientAPI := make(chan gtsmodel.FromClientAPI, 1000) + fromFederator := make(chan gtsmodel.FromFederator, 1000) + + statusProcessor := status.New(db, tc, config, fromClientAPI, log) + return &processor{ - // toClientAPI: make(chan gtsmodel.ToClientAPI, 100), - fromClientAPI: make(chan gtsmodel.FromClientAPI, 100), - // toFederator: make(chan gtsmodel.ToFederator, 100), - fromFederator: make(chan gtsmodel.FromFederator, 100), + fromClientAPI: fromClientAPI, + fromFederator: fromFederator, federator: federator, stop: make(chan interface{}), log: log, @@ -213,6 +222,8 @@ func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator f storage: storage, timelineManager: timelineManager, db: db, + + statusProcessor: statusProcessor, } } diff --git a/internal/processing/search.go b/internal/processing/search.go index a712e5e..9cc60cd 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -27,12 +27,13 @@ import ( "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/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode) { +func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) { l := p.log.WithFields(logrus.Fields{ "func": "SearchGet", "query": searchQuery.Query, @@ -164,7 +165,7 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve // first turn it into a gtsmodel.Status status, err := p.tc.ASStatusToStatus(statusable) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } // put it in the DB so it gets a UUID diff --git a/internal/processing/status.go b/internal/processing/status.go index 8979728..6848436 100644 --- a/internal/processing/status.go +++ b/internal/processing/status.go @@ -19,531 +19,43 @@ package processing import ( - "errors" - "fmt" - "time" - - "github.com/google/uuid" 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/gtserror" "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) { - uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.config.Host) - thisStatusID := uuid.NewString() - 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, - Content: util.HTMLFormat(form.Status), - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Local: true, - AccountID: auth.Account.ID, - ContentWarning: form.SpoilerText, - ActivityStreamsType: gtsmodel.ActivityStreamsNote, - Sensitive: form.Sensitive, - Language: form.Language, - CreatedWithApplicationID: auth.Application.ID, - Text: form.Status, - } - - // check if replyToID is ok - if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil { - return nil, err - } - - // check if mediaIDs are ok - if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil { - return nil, err - } - - // check if visibility settings are ok - if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil { - return nil, err - } - - // handle language settings - if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil { - return nil, err - } - - // handle mentions - if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil { - return nil, err - } - - if err := p.processTags(form, auth.Account.ID, newStatus); err != nil { - return nil, err - } - - if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil { - return nil, 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, 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, err - } - } - - // put the new status in the appropriate channel for async processing - p.fromClientAPI <- gtsmodel.FromClientAPI{ - APObjectType: newStatus.ActivityStreamsType, - APActivityType: gtsmodel.ActivityStreamsCreate, - GTSModel: newStatus, - } - - // return the frontend representation of the new status to the submitter - return p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil) +func (p *processor) StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) { + return p.statusProcessor.Create(authed.Account, authed.Application, form) } func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { - 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 { - return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err) - } - - if targetStatus.AccountID != authed.Account.ID { - return nil, errors.New("status doesn't belong to requesting account") - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) - } - - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) - if err != nil { - return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) - } - - if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil { - return nil, fmt.Errorf("error deleting status from the database: %s", err) - } - - return mastoStatus, nil + return p.statusProcessor.Delete(authed.Account, targetStatusID) } func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { - 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, 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, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) - } - - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) - } - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) - } - - if !visible { - return nil, errors.New("status is not visible") - } - - // is the status faveable? - if targetStatus.VisibilityAdvanced != nil { - if !targetStatus.VisibilityAdvanced.Likeable { - return nil, 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.Status{} - if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: authed.Account.ID}}, gtsFave); err == nil { - // we already have a fave for this status - newFave = false - } - - if newFave { - thisFaveID := uuid.NewString() - - // we need to create a new fave in the database - gtsFave := >smodel.StatusFave{ - ID: thisFaveID, - AccountID: authed.Account.ID, - TargetAccountID: targetAccount.ID, - StatusID: targetStatus.ID, - URI: util.GenerateURIForLike(authed.Account.Username, p.config.Protocol, p.config.Host, thisFaveID), - GTSStatus: targetStatus, - GTSTargetAccount: targetAccount, - GTSFavingAccount: authed.Account, - } - - if err := p.db.Put(gtsFave); err != nil { - return nil, err - } - - // send the new fave through the processor channel for federation etc - p.fromClientAPI <- gtsmodel.FromClientAPI{ - APObjectType: gtsmodel.ActivityStreamsLike, - APActivityType: gtsmodel.ActivityStreamsCreate, - GTSModel: gtsFave, - OriginAccount: authed.Account, - TargetAccount: targetAccount, - } - } - - // return the mastodon representation of the target status - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) - if err != nil { - return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) - } - - return mastoStatus, nil + return p.statusProcessor.Fave(authed.Account, targetStatusID) } -func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode) { - 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, 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, NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - - if !visible { - return nil, NewErrorNotFound(errors.New("status is not visible")) - } - - if targetStatus.VisibilityAdvanced != nil { - if !targetStatus.VisibilityAdvanced.Boostable { - return nil, 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, authed.Account) - if err != nil { - return nil, NewErrorInternalError(err) - } - - boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID - boostWrapperStatus.GTSBoostedAccount = targetAccount - - // put the boost in the database - if err := p.db.Put(boostWrapperStatus); err != nil { - return nil, NewErrorInternalError(err) - } - - // send it to the processor for async processing - p.fromClientAPI <- gtsmodel.FromClientAPI{ - APObjectType: gtsmodel.ActivityStreamsAnnounce, - APActivityType: gtsmodel.ActivityStreamsCreate, - GTSModel: boostWrapperStatus, - OriginAccount: authed.Account, - TargetAccount: targetAccount, - } - - // return the frontend representation of the new status to the submitter - mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, authed.Account, authed.Account, targetAccount, nil, targetStatus) - if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) - } - - return mastoStatus, nil +func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + return p.statusProcessor.Boost(authed.Account, authed.Application, targetStatusID) } -func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, ErrorWithCode) { - 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, 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, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err)) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching related accounts for status %s: %s", targetStatusID, err)) - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - - if !visible { - return nil, 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, 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(authed.Account.ID, acc.ID) - if err != nil { - return nil, 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, NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err)) - } - mastoAccounts = append(mastoAccounts, mastoAccount) - } - - return mastoAccounts, nil +func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { + return p.statusProcessor.BoostedBy(authed.Account, targetStatusID) } func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) { - 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, 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, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) - } - - if !visible { - return nil, 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, 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(authed.Account.ID, acc.ID) - if err != nil { - return nil, 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, fmt.Errorf("error converting account to api model: %s", err) - } - mastoAccounts = append(mastoAccounts, mastoAccount) - } - - return mastoAccounts, nil + return p.statusProcessor.FavedBy(authed.Account, targetStatusID) } func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { - 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, 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, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) - } - - if !visible { - return nil, 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, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) - if err != nil { - return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) - } - - return mastoStatus, nil - + return p.statusProcessor.Get(authed.Account, targetStatusID) } func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) { - 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, 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, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) - } - - l.Trace("going to get relevant accounts") - relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) - if err != nil { - return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err) - } - - l.Trace("going to see if status is visible") - visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that - if err != nil { - return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) - } - - if !visible { - return nil, errors.New("status is not visible") - } - - // is the status faveable? - if targetStatus.VisibilityAdvanced != nil { - if !targetStatus.VisibilityAdvanced.Likeable { - return nil, errors.New("status is not faveable") - } - } - - // it's visible! it's faveable! so let's unfave the FUCK out of it - _, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID) - if err != nil { - return nil, fmt.Errorf("error unfaveing status: %s", err) - } - - var boostOfStatus *gtsmodel.Status - if targetStatus.BoostOfID != "" { - boostOfStatus = >smodel.Status{} - if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil { - return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err) - } - } - - mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) - if err != nil { - return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err) - } - - return mastoStatus, nil + return p.statusProcessor.Unfave(authed.Account, targetStatusID) } -func (p *processor) StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, ErrorWithCode) { - return &apimodel.Context{}, nil +func (p *processor) StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { + return p.statusProcessor.Context(authed.Account, targetStatusID) } diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go new file mode 100644 index 0000000..1abdbc2 --- /dev/null +++ b/internal/processing/status/boost.go @@ -0,0 +1,79 @@ +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 get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) + } + + l.Trace("going to see if status is visible") + visible, err := p.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // 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")) + } + + 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 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, account, targetAccount, nil, targetStatus) + 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 0000000..8f6cfb5 --- /dev/null +++ b/internal/processing/status/boostedby.go @@ -0,0 +1,74 @@ +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 get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching related accounts for status %s: %s", targetStatusID, err)) + } + + l.Trace("going to see if status is visible") + visible, err := p.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + 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 0000000..f4d62ea --- /dev/null +++ b/internal/processing/status/context.go @@ -0,0 +1,11 @@ +package status + +import ( + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "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) { + return &apimodel.Context{}, nil +} diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go new file mode 100644 index 0000000..e57abfe --- /dev/null +++ b/internal/processing/status/create.go @@ -0,0 +1,98 @@ +package status + +import ( + "fmt" + "time" + + "github.com/google/uuid" + 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/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 := uuid.NewString() + 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, + Content: util.HTMLFormat(form.Status), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Local: true, + AccountID: account.ID, + 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) + } + + // 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) + } + } + + // put the new status in the appropriate channel 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, account, nil, newStatus.GTSReplyToAccount, nil) + 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 0000000..569edba --- /dev/null +++ b/internal/processing/status/delete.go @@ -0,0 +1,55 @@ +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) 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 { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + + if targetStatus.AccountID != account.ID { + return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account")) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, 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)) + } + } + + mastoStatus, err := p.tc.StatusToMasto(targetStatus, account, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + 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, targetStatus); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err)) + } + + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsNote, + APActivityType: gtsmodel.ActivityStreamsDelete, + GTSModel: targetStatus, + OriginAccount: account, + } + + return mastoStatus, nil +} diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go new file mode 100644 index 0000000..fc10279 --- /dev/null +++ b/internal/processing/status/fave.go @@ -0,0 +1,104 @@ +package status + +import ( + "errors" + "fmt" + + "github.com/google/uuid" + 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/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)) + } + + l.Trace("going to get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, 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.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // 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 := uuid.NewString() + + // 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 the new fave through the processor channel for federation etc + 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, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + 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 0000000..9b00ce8 --- /dev/null +++ b/internal/processing/status/favedby.go @@ -0,0 +1,74 @@ +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 get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) + } + + l.Trace("going to see if status is visible") + visible, err := p.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // 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")) + } + + // 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 0000000..e36304c --- /dev/null +++ b/internal/processing/status/get.go @@ -0,0 +1,58 @@ +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 get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) + } + + l.Trace("going to see if status is visible") + visible, err := p.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // 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, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + 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 0000000..e03ccf1 --- /dev/null +++ b/internal/processing/status/status.go @@ -0,0 +1,52 @@ +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" +) + +// 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) + // 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 + 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, + fromClientAPI: fromClientAPI, + log: log, + } +} diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go new file mode 100644 index 0000000..a304d80 --- /dev/null +++ b/internal/processing/status/unfave.go @@ -0,0 +1,93 @@ +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 get relevant accounts") + relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)) + } + + l.Trace("going to see if status is visible") + visible, err := p.db.StatusVisible(targetStatus, targetAccount, account, relevantAccounts) // 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")) + } + + // 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 + _, err = p.db.UnfaveStatus(targetStatus, account.ID) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) + } + + // send the unfave through the processor channel for federation etc + p.fromClientAPI <- gtsmodel.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsLike, + APActivityType: gtsmodel.ActivityStreamsUndo, + GTSModel: gtsFave, + OriginAccount: account, + TargetAccount: targetAccount, + } + } + + // return the status (whatever its state) back to the caller + 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, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus) + 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 0000000..71ecac6 --- /dev/null +++ b/internal/processing/status/util.go @@ -0,0 +1,230 @@ +package status + +import ( + "errors" + "fmt" + + 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/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 { + 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 +} diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 3440430..6db12c1 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -25,29 +25,30 @@ import ( "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/oauth" ) -func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, ErrorWithCode) { +func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) { statuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return statuses, nil } -func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, ErrorWithCode) { +func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) { statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } s, err := p.filterStatuses(authed, statuses) if err != nil { - return nil, NewErrorInternalError(err) + return nil, gtserror.NewErrorInternalError(err) } return s, nil @@ -64,7 +65,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat l.Debugf("skipping status %s because account %s can't be found in the db", s.ID, s.AccountID) continue } - return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err)) } relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s) @@ -75,7 +76,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat visible, err := p.db.StatusVisible(s, targetAccount, authed.Account, relevantAccounts) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err)) } if !visible { continue @@ -89,7 +90,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat l.Debugf("skipping status %s because status %s can't be found in the db", s.ID, s.BoostOfID) continue } - return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err)) } boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs) if err != nil { @@ -99,7 +100,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts) if err != nil { - return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err)) + return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err)) } if boostedVisible {