summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>2021-06-13 18:42:28 +0200
committerLibravatar GitHub <noreply@github.com>2021-06-13 18:42:28 +0200
commitb4288f3c47a9ff9254b933dcb9ee7274d4a4135c (patch)
tree3fe1bb1ab8d4b8c5d9a83df708e5088f35c3150a /internal/processing
parentTidy + timeline embetterment (#38) (diff)
downloadgotosocial-b4288f3c47a9ff9254b933dcb9ee7274d4a4135c.tar.xz
Timeline manager (#40)
* start messing about with timeline manager * i have no idea what i'm doing * i continue to not know what i'm doing * it's coming along * bit more progress * update timeline with new posts as they come in * lint and fmt * Select accounts where empty string * restructure a bunch, get unfaves working * moving stuff around * federate status deletes properly * mention regex better but not 100% there * fix regex * some more hacking away at the timeline code phew * fix up some little things * i can't even * more timeline stuff * move to ulid * fiddley * some lil fixes for kibou compatibility * timelines working pretty alright! * tidy + lint
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/account.go99
-rw-r--r--internal/processing/admin.go7
-rw-r--r--internal/processing/app.go12
-rw-r--r--internal/processing/error.go124
-rw-r--r--internal/processing/federation.go77
-rw-r--r--internal/processing/followrequest.go23
-rw-r--r--internal/processing/fromclientapi.go104
-rw-r--r--internal/processing/fromcommon.go124
-rw-r--r--internal/processing/fromfederator.go56
-rw-r--r--internal/processing/instance.go7
-rw-r--r--internal/processing/media.go55
-rw-r--r--internal/processing/notification.go5
-rw-r--r--internal/processing/processor.go140
-rw-r--r--internal/processing/search.go27
-rw-r--r--internal/processing/status.go516
-rw-r--r--internal/processing/synchronous/status/boost.go79
-rw-r--r--internal/processing/synchronous/status/boostedby.go74
-rw-r--r--internal/processing/synchronous/status/context.go14
-rw-r--r--internal/processing/synchronous/status/create.go105
-rw-r--r--internal/processing/synchronous/status/delete.go61
-rw-r--r--internal/processing/synchronous/status/fave.go107
-rw-r--r--internal/processing/synchronous/status/favedby.go74
-rw-r--r--internal/processing/synchronous/status/get.go58
-rw-r--r--internal/processing/synchronous/status/status.go52
-rw-r--r--internal/processing/synchronous/status/unfave.go92
-rw-r--r--internal/processing/synchronous/status/util.go269
-rw-r--r--internal/processing/timeline.go168
-rw-r--r--internal/processing/util.go222
28 files changed, 1671 insertions, 1080 deletions
diff --git a/internal/processing/account.go b/internal/processing/account.go
index 92ccf10fa..870734184 100644
--- a/internal/processing/account.go
+++ b/internal/processing/account.go
@@ -22,10 +22,11 @@ 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/id"
"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 := &gtsmodel.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)
+ visible, err := p.db.StatusVisible(&s, 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 := &gtsmodel.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)
+ boostedVisible, err := p.db.StatusVisible(bs, 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,29 +267,29 @@ 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{}
accounts := []apimodel.Account{}
- if err := p.db.GetFollowersByAccountID(targetAccountID, &followers); err != nil {
+ if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
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 := &gtsmodel.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
@@ -425,8 +426,10 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
}
// make the follow request
-
- newFollowID := uuid.NewString()
+ newFollowID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
fr := &gtsmodel.FollowRequest{
ID: newFollowID,
@@ -445,13 +448,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 +473,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 := &gtsmodel.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 +501,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 +516,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/admin.go b/internal/processing/admin.go
index 78979a228..6ee3a059f 100644
--- a/internal/processing/admin.go
+++ b/internal/processing/admin.go
@@ -25,6 +25,7 @@ import (
"io"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -53,6 +54,12 @@ func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCre
return nil, fmt.Errorf("error reading emoji: %s", err)
}
+ emojiID, err := id.NewULID()
+ if err != nil {
+ return nil, err
+ }
+ emoji.ID = emojiID
+
mastoEmoji, err := p.tc.EmojiToMasto(emoji)
if err != nil {
return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
diff --git a/internal/processing/app.go b/internal/processing/app.go
index 47fce051b..7da5344ac 100644
--- a/internal/processing/app.go
+++ b/internal/processing/app.go
@@ -22,6 +22,7 @@ import (
"github.com/google/uuid"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@@ -35,12 +36,21 @@ func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCrea
}
// generate new IDs for this application and its associated client
- clientID := uuid.NewString()
+ clientID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, err
+ }
clientSecret := uuid.NewString()
vapidKey := uuid.NewString()
+ appID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, err
+ }
+
// generate the application to put in the database
app := &gtsmodel.Application{
+ ID: appID,
Name: form.ClientName,
Website: form.Website,
RedirectURI: form.RedirectURIs,
diff --git a/internal/processing/error.go b/internal/processing/error.go
deleted file mode 100644
index 1fea01d08..000000000
--- a/internal/processing/error.go
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-*/
-
-package processing
-
-import (
- "errors"
- "net/http"
- "strings"
-)
-
-// ErrorWithCode 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 {
- // 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
- // Safe returns the API-safe version of the error for serialization towards a client.
- // There's not much point logging this internally because it won't contain much helpful information.
- Safe() string
- // Code returns the status code for serving to a client.
- Code() int
-}
-
-type errorWithCode struct {
- original error
- safe error
- code int
-}
-
-func (e errorWithCode) Error() string {
- return e.original.Error()
-}
-
-func (e errorWithCode) Safe() string {
- return e.safe.Error()
-}
-
-func (e errorWithCode) 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 {
- safe := "bad request"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusBadRequest,
- }
-}
-
-// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
-func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
- safe := "not authorized"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusUnauthorized,
- }
-}
-
-// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
-func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
- safe := "forbidden"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusForbidden,
- }
-}
-
-// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
-func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
- safe := "404 not found"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusNotFound,
- }
-}
-
-// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
-func NewErrorInternalError(original error, helpText ...string) ErrorWithCode {
- safe := "internal server error"
- if helpText != nil {
- safe = safe + ": " + strings.Join(helpText, ": ")
- }
- return errorWithCode{
- original: original,
- safe: errors.New(safe),
- code: http.StatusInternalServerError,
- }
-}
diff --git a/internal/processing/federation.go b/internal/processing/federation.go
index b93455d6e..1c0d67fc8 100644
--- a/internal/processing/federation.go
+++ b/internal/processing/federation.go
@@ -27,7 +27,9 @@ 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/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -73,13 +75,18 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
}
- // shove it in the database for later
+ requestingAccountID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, err
+ }
+ requestingAccount.ID = requestingAccountID
+
if err := p.db.Put(requestingAccount); err != nil {
return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
}
// put it in our channel to queue it for async processing
- p.FromFederator() <- gtsmodel.FromFederator{
+ p.fromFederator <- gtsmodel.FromFederator{
APObjectType: gtsmodel.ActivityStreamsProfile,
APActivityType: gtsmodel.ActivityStreamsCreate,
GTSModel: requestingAccount,
@@ -88,141 +95,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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.Status{}
@@ -230,27 +237,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 := &gtsmodel.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 7e606f5da..5eb9fd6ad 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 := &gtsmodel.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 := &gtsmodel.Account{}
if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
- return nil, NewErrorInternalError(err)
+ return nil, gtserror.NewErrorInternalError(err)
}
targetAccount := &gtsmodel.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 0d8b73eb0..d171e593a 100644
--- a/internal/processing/fromclientapi.go
+++ b/internal/processing/fromclientapi.go
@@ -40,6 +40,10 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
return errors.New("note was not parseable as *gtsmodel.Status")
}
+ if err := p.timelineStatus(status); err != nil {
+ return err
+ }
+
if err := p.notifyStatus(status); err != nil {
return err
}
@@ -47,7 +51,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
if status.VisibilityAdvanced != nil && status.VisibilityAdvanced.Federated {
return p.federateStatus(status)
}
- return nil
case gtsmodel.ActivityStreamsFollow:
// CREATE FOLLOW REQUEST
followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
@@ -124,6 +127,29 @@ 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)
+ }
+ case gtsmodel.ActivityStreamsDelete:
+ // DELETE
+ switch clientMsg.APObjectType {
+ case gtsmodel.ActivityStreamsNote:
+ // DELETE STATUS/NOTE
+ statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return errors.New("note was not parseable as *gtsmodel.Status")
+ }
+
+ if err := p.deleteStatusFromTimelines(statusToDelete); err != nil {
+ return err
+ }
+
+ return p.federateStatusDelete(statusToDelete, clientMsg.OriginAccount)
}
}
return nil
@@ -144,6 +170,43 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
return err
}
+func (p *processor) federateStatusDelete(status *gtsmodel.Status, originAccount *gtsmodel.Account) error {
+ asStatus, err := p.tc.StatusToAS(status)
+ if err != nil {
+ return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)
+ }
+
+ outboxIRI, err := url.Parse(originAccount.OutboxURI)
+ if err != nil {
+ return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
+ }
+
+ actorIRI, err := url.Parse(originAccount.URI)
+ if err != nil {
+ return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", originAccount.URI, err)
+ }
+
+ // create a delete and set the appropriate actor on it
+ delete := streams.NewActivityStreamsDelete()
+
+ // set the actor for the delete
+ deleteActor := streams.NewActivityStreamsActorProperty()
+ deleteActor.AppendIRI(actorIRI)
+ delete.SetActivityStreamsActor(deleteActor)
+
+ // Set the status as the 'object' property.
+ deleteObject := streams.NewActivityStreamsObjectProperty()
+ deleteObject.AppendActivityStreamsNote(asStatus)
+ delete.SetActivityStreamsObject(deleteObject)
+
+ // set the to and cc as the original to/cc of the original status
+ delete.SetActivityStreamsTo(asStatus.GetActivityStreamsTo())
+ delete.SetActivityStreamsCc(asStatus.GetActivityStreamsCc())
+
+ _, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, delete)
+ return err
+}
+
func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
// if both accounts are local there's nothing to do here
if originAccount.Domain == "" && targetAccount.Domain == "" {
@@ -207,6 +270,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 bdb2a599b..85531d20b 100644
--- a/internal/processing/fromcommon.go
+++ b/internal/processing/fromcommon.go
@@ -20,9 +20,12 @@ package processing
import (
"fmt"
+ "strings"
+ "sync"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
)
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
@@ -77,7 +80,13 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {
}
// if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it
+ notifID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+
notif := &gtsmodel.Notification{
+ ID: notifID,
NotificationType: gtsmodel.NotificationMention,
TargetAccountID: m.TargetAccountID,
OriginAccountID: status.AccountID,
@@ -98,7 +107,13 @@ func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, r
return nil
}
+ notifID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+
notif := &gtsmodel.Notification{
+ ID: notifID,
NotificationType: gtsmodel.NotificationFollowRequest,
TargetAccountID: followRequest.TargetAccountID,
OriginAccountID: followRequest.AccountID,
@@ -127,7 +142,13 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm
}
// now create the new follow notification
+ notifID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+
notif := &gtsmodel.Notification{
+ ID: notifID,
NotificationType: gtsmodel.NotificationFollow,
TargetAccountID: follow.TargetAccountID,
OriginAccountID: follow.AccountID,
@@ -145,7 +166,13 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm
return nil
}
+ notifID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+
notif := &gtsmodel.Notification{
+ ID: notifID,
NotificationType: gtsmodel.NotificationFave,
TargetAccountID: fave.TargetAccountID,
OriginAccountID: fave.AccountID,
@@ -198,7 +225,13 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
}
// now create the new reblog notification
+ notifID, err := id.NewULID()
+ if err != nil {
+ return err
+ }
+
notif := &gtsmodel.Notification{
+ ID: notifID,
NotificationType: gtsmodel.NotificationReblog,
TargetAccountID: boostedAcct.ID,
OriginAccountID: status.AccountID,
@@ -211,3 +244,94 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
return nil
}
+
+func (p *processor) timelineStatus(status *gtsmodel.Status) error {
+ // make sure the author account is pinned onto the status
+ if status.GTSAuthorAccount == nil {
+ a := &gtsmodel.Account{}
+ if err := p.db.GetByID(status.AccountID, a); err != nil {
+ return fmt.Errorf("timelineStatus: error getting author account with id %s: %s", status.AccountID, err)
+ }
+ status.GTSAuthorAccount = a
+ }
+
+ // get all relevant accounts here once
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(status)
+ if err != nil {
+ return fmt.Errorf("timelineStatus: error getting relevant accounts from status: %s", err)
+ }
+
+ // get local followers of the account that posted the status
+ followers := []gtsmodel.Follow{}
+ if err := p.db.GetFollowersByAccountID(status.AccountID, &followers, true); err != nil {
+ return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)
+ }
+
+ // if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline
+ if status.GTSAuthorAccount.Domain == "" {
+ followers = append(followers, gtsmodel.Follow{
+ AccountID: status.AccountID,
+ })
+ }
+
+ wg := sync.WaitGroup{}
+ wg.Add(len(followers))
+ errors := make(chan error, len(followers))
+
+ for _, f := range followers {
+ go p.timelineStatusForAccount(status, f.AccountID, relevantAccounts, errors, &wg)
+ }
+
+ // read any errors that come in from the async functions
+ errs := []string{}
+ go func() {
+ for range errors {
+ e := <-errors
+ if e != nil {
+ errs = append(errs, e.Error())
+ }
+ }
+ }()
+
+ // wait til all functions have returned and then close the error channel
+ wg.Wait()
+ close(errors)
+
+ if len(errs) != 0 {
+ // we have some errors
+ return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %s", strings.Join(errs, ";"))
+ }
+
+ // no errors, nice
+ return nil
+}
+
+func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID string, relevantAccounts *gtsmodel.RelevantAccounts, errors chan error, wg *sync.WaitGroup) {
+ defer wg.Done()
+
+ // get the targetAccount
+ timelineAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(accountID, timelineAccount); err != nil {
+ errors <- fmt.Errorf("timelineStatus: error getting account for timeline with id %s: %s", accountID, err)
+ return
+ }
+
+ // make sure the status is visible
+ visible, err := p.db.StatusVisible(status, timelineAccount, relevantAccounts)
+ if err != nil {
+ errors <- fmt.Errorf("timelineStatus: error getting visibility for status for timeline with id %s: %s", accountID, err)
+ return
+ }
+
+ if !visible {
+ return
+ }
+
+ if err := p.timelineManager.IngestAndPrepare(status, timelineAccount.ID); err != nil {
+ errors <- fmt.Errorf("initTimelineFor: error ingesting status %s: %s", status.ID, err)
+ }
+}
+
+func (p *processor) deleteStatusFromTimelines(status *gtsmodel.Status) error {
+ return p.timelineManager.WipeStatusFromAllTimelines(status.ID)
+}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index 479bdec33..f010a7aa1 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -23,10 +23,10 @@ import (
"fmt"
"net/url"
- "github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
)
func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error {
@@ -56,9 +56,14 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return fmt.Errorf("error updating dereferenced status in the db: %s", err)
}
+ if err := p.timelineStatus(incomingStatus); err != nil {
+ return err
+ }
+
if err := p.notifyStatus(incomingStatus); err != nil {
return err
}
+
case gtsmodel.ActivityStreamsProfile:
// CREATE AN ACCOUNT
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
@@ -104,6 +109,12 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
return fmt.Errorf("error dereferencing announce from federator: %s", err)
}
+ incomingAnnounceID, err := id.NewULIDFromTime(incomingAnnounce.CreatedAt)
+ if err != nil {
+ return err
+ }
+ incomingAnnounce.ID = incomingAnnounceID
+
if err := p.db.Put(incomingAnnounce); err != nil {
if _, ok := err.(db.ErrAlreadyExists); !ok {
return fmt.Errorf("error adding dereferenced announce to the db: %s", err)
@@ -141,6 +152,11 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
// 1. delete all media associated with status
// 2. delete boosts of status
// 3. etc etc etc
+ statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
+ if !ok {
+ return errors.New("note was not parseable as *gtsmodel.Status")
+ }
+ return p.deleteStatusFromTimelines(statusToDelete)
case gtsmodel.ActivityStreamsProfile:
// DELETE A PROFILE/ACCOUNT
// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
@@ -202,7 +218,11 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
// the status should have an ID by now, but just in case it doesn't let's generate one here
// because we'll need it further down
if status.ID == "" {
- status.ID = uuid.NewString()
+ newID, err := id.NewULIDFromTime(status.CreatedAt)
+ if err != nil {
+ return err
+ }
+ status.ID = newID
}
// 1. Media attachments.
@@ -257,6 +277,14 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
mentions := []string{}
for _, m := range status.GTSMentions {
+ if m.ID == "" {
+ mID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ m.ID = mID
+ }
+
uri, err := url.Parse(m.MentionedAccountURI)
if err != nil {
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
@@ -288,6 +316,12 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
continue
}
+ targetAccountID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ targetAccount.ID = targetAccountID
+
if err := p.db.Put(targetAccount); err != nil {
return fmt.Errorf("db error inserting account with uri %s", uri.String())
}
@@ -354,12 +388,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
}
// we don't have it so we need to dereference it
- remoteStatusID, err := url.Parse(announce.GTSBoostedStatus.URI)
+ remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
}
- statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusID)
+ statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
if err != nil {
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
}
@@ -387,7 +421,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
}
- // insert the dereferenced account so it gets an ID etc
+ accountID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ account.ID = accountID
+
if err := p.db.Put(account); err != nil {
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
}
@@ -403,7 +442,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
}
- // put it in the db already so it gets an ID generated for it
+ boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
+ if err != nil {
+ return nil
+ }
+ boostedStatus.ID = boostedStatusID
+
if err := p.db.Put(boostedStatus); err != nil {
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
}
diff --git a/internal/processing/instance.go b/internal/processing/instance.go
index e928bf642..9381a7315 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 := &gtsmodel.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 255f49067..4f15632c1 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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 44e3885b5..6ad974126 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 b31c37be3..1ccf71e34 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -28,9 +28,12 @@ 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/synchronous/status"
+ "github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
@@ -41,14 +44,6 @@ import (
// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
// for clean distribution of messages without slowing down the client API and harming the user experience.
type Processor interface {
- // ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
- // ToClientAPI() chan gtsmodel.ToClientAPI
- // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
- FromClientAPI() chan gtsmodel.FromClientAPI
- // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
- // ToFederator() chan gtsmodel.ToFederator
- // FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
- FromFederator() chan gtsmodel.FromFederator
// Start starts the Processor, reading from its channels and passing messages back and forth.
Start() error
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
@@ -70,17 +65,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)
@@ -92,25 +87,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)
@@ -119,9 +114,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.
@@ -129,12 +124,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.StatusTimelineResponse, 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
@@ -146,22 +141,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.
//
@@ -178,55 +173,50 @@ 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
- fromFederator chan gtsmodel.FromFederator
- federator federation.Federator
- stop chan interface{}
- log *logrus.Logger
- config *config.Config
- tc typeutils.TypeConverter
- oauthServer oauth.Server
- mediaHandler media.Handler
- storage blob.Storage
- db db.DB
-}
+ fromClientAPI chan gtsmodel.FromClientAPI
+ fromFederator chan gtsmodel.FromFederator
+ federator federation.Federator
+ stop chan interface{}
+ log *logrus.Logger
+ config *config.Config
+ tc typeutils.TypeConverter
+ oauthServer oauth.Server
+ mediaHandler media.Handler
+ storage blob.Storage
+ timelineManager timeline.Manager
+ db db.DB
-// 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, db db.DB, log *logrus.Logger) Processor {
- 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),
- federator: federator,
- stop: make(chan interface{}),
- log: log,
- config: config,
- tc: tc,
- oauthServer: oauthServer,
- mediaHandler: mediaHandler,
- storage: storage,
- db: db,
- }
+ /*
+ SUB-PROCESSORS
+ */
+
+ statusProcessor status.Processor
}
-// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI {
-// return p.toClientAPI
-// }
+// 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 {
-func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI {
- return p.fromClientAPI
-}
+ fromClientAPI := make(chan gtsmodel.FromClientAPI, 1000)
+ fromFederator := make(chan gtsmodel.FromFederator, 1000)
-// func (p *processor) ToFederator() chan gtsmodel.ToFederator {
-// return p.toFederator
-// }
+ statusProcessor := status.New(db, tc, config, fromClientAPI, log)
-func (p *processor) FromFederator() chan gtsmodel.FromFederator {
- return p.fromFederator
+ return &processor{
+ fromClientAPI: fromClientAPI,
+ fromFederator: fromFederator,
+ federator: federator,
+ stop: make(chan interface{}),
+ log: log,
+ config: config,
+ tc: tc,
+ oauthServer: oauthServer,
+ mediaHandler: mediaHandler,
+ storage: storage,
+ timelineManager: timelineManager,
+ db: db,
+
+ statusProcessor: statusProcessor,
+ }
}
// Start starts the Processor, reading from its channels and passing messages back and forth.
@@ -250,7 +240,7 @@ func (p *processor) Start() error {
}
}
}()
- return nil
+ return p.initTimelines()
}
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
diff --git a/internal/processing/search.go b/internal/processing/search.go
index a712e5e1a..d518a0310 100644
--- a/internal/processing/search.go
+++ b/internal/processing/search.go
@@ -27,12 +27,14 @@ 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/id"
"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,
@@ -108,7 +110,7 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu
if err != nil {
continue
}
- if visible, err := p.db.StatusVisible(foundStatus, statusOwner, authed.Account, relevantAccounts); !visible || err != nil {
+ if visible, err := p.db.StatusVisible(foundStatus, authed.Account, relevantAccounts); !visible || err != nil {
continue
}
@@ -164,10 +166,15 @@ 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
+ statusID, err := id.NewULIDFromTime(status.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ status.ID = statusID
+
if err := p.db.Put(status); err != nil {
return nil, fmt.Errorf("error putting status in the db: %s", err)
}
@@ -210,6 +217,12 @@ func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve
return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
}
+ accountID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, err
+ }
+ account.ID = accountID
+
if err := p.db.Put(account); err != nil {
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
}
@@ -280,6 +293,12 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r
return nil, fmt.Errorf("searchAccountByMention: error converting account with uri %s: %s", acctURI.String(), err)
}
+ foundAccountID, err := id.NewULID()
+ if err != nil {
+ return nil, err
+ }
+ foundAccount.ID = foundAccountID
+
// put this new account in our database
if err := p.db.Put(foundAccount); err != nil {
return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err)
diff --git a/internal/processing/status.go b/internal/processing/status.go
index 897972839..6848436d4 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 := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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/synchronous/status/boost.go b/internal/processing/synchronous/status/boost.go
new file mode 100644
index 000000000..a746e9fd8
--- /dev/null
+++ b/internal/processing/synchronous/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 := &gtsmodel.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 := &gtsmodel.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, account, relevantAccounts)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ if targetStatus.VisibilityAdvanced != nil {
+ if !targetStatus.VisibilityAdvanced.Boostable {
+ return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))
+ }
+ }
+
+ // it's visible! it's boostable! so let's boost the FUCK out of it
+ boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ boostWrapperStatus.CreatedWithApplicationID = application.ID
+ boostWrapperStatus.GTSBoostedAccount = targetAccount
+
+ // put the boost in the database
+ if err := p.db.Put(boostWrapperStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsAnnounce,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: boostWrapperStatus,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account, 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/synchronous/status/boostedby.go b/internal/processing/synchronous/status/boostedby.go
new file mode 100644
index 000000000..8ebfcebc0
--- /dev/null
+++ b/internal/processing/synchronous/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 := &gtsmodel.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 := &gtsmodel.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, account, relevantAccounts)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible"))
+ }
+
+ // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
+ favingAccounts, err := p.db.WhoBoostedStatus(targetStatus)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))
+ }
+
+ // filter the list so the user doesn't see accounts they blocked or which blocked them
+ filteredAccounts := []*gtsmodel.Account{}
+ for _, acc := range favingAccounts {
+ blocked, err := p.db.Blocked(account.ID, acc.ID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))
+ }
+ if !blocked {
+ filteredAccounts = append(filteredAccounts, acc)
+ }
+ }
+
+ // TODO: filter other things here? suspended? muted? silenced?
+
+ // now we can return the masto representation of those accounts
+ mastoAccounts := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ mastoAccount, err := p.tc.AccountToMastoPublic(acc)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
+ }
+ mastoAccounts = append(mastoAccounts, mastoAccount)
+ }
+
+ return mastoAccounts, nil
+}
diff --git a/internal/processing/synchronous/status/context.go b/internal/processing/synchronous/status/context.go
new file mode 100644
index 000000000..cac86815e
--- /dev/null
+++ b/internal/processing/synchronous/status/context.go
@@ -0,0 +1,14 @@
+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{
+ Ancestors: []apimodel.Status{},
+ Descendants: []apimodel.Status{},
+ }, nil
+}
diff --git a/internal/processing/synchronous/status/create.go b/internal/processing/synchronous/status/create.go
new file mode 100644
index 000000000..07f670d1a
--- /dev/null
+++ b/internal/processing/synchronous/status/create.go
@@ -0,0 +1,105 @@
+package status
+
+import (
+ "fmt"
+ "time"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
+ uris := util.GenerateURIsForAccount(account.Username, p.config.Protocol, p.config.Host)
+ thisStatusID, err := id.NewULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
+ thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
+
+ newStatus := &gtsmodel.Status{
+ ID: thisStatusID,
+ URI: thisStatusURI,
+ URL: thisStatusURL,
+ 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)
+ }
+
+ if err := p.processContent(form, account.ID, newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // put the new status in the database, generating an ID for it in the process
+ if err := p.db.Put(newStatus); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // change the status ID of the media attachments to the new status
+ for _, a := range newStatus.GTSMediaAttachments {
+ a.StatusID = newStatus.ID
+ a.UpdatedAt = time.Now()
+ if err := p.db.UpdateByID(a.ID, a); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: newStatus,
+ OriginAccount: account,
+ }
+
+ // return the frontend representation of the new status to the submitter
+ mastoStatus, err := p.tc.StatusToMasto(newStatus, account, 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/synchronous/status/delete.go b/internal/processing/synchronous/status/delete.go
new file mode 100644
index 000000000..7e251080a
--- /dev/null
+++ b/internal/processing/synchronous/status/delete.go
@@ -0,0 +1,61 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+)
+
+func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusDelete")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.Status{}
+ if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ // status is already gone
+ return nil, nil
+ }
+
+ if targetStatus.AccountID != account.ID {
+ return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
+ }
+
+ 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 = &gtsmodel.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, &gtsmodel.Status{}); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsDelete,
+ GTSModel: targetStatus,
+ OriginAccount: account,
+ }
+
+ return mastoStatus, nil
+}
diff --git a/internal/processing/synchronous/status/fave.go b/internal/processing/synchronous/status/fave.go
new file mode 100644
index 000000000..b4622abbc
--- /dev/null
+++ b/internal/processing/synchronous/status/fave.go
@@ -0,0 +1,107 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ l := p.log.WithField("func", "StatusFave")
+ l.Tracef("going to search for target status %s", targetStatusID)
+ targetStatus := &gtsmodel.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 := &gtsmodel.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 = &gtsmodel.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, 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 := &gtsmodel.StatusFave{}
+ if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil {
+ // we already have a fave for this status
+ newFave = false
+ }
+
+ if newFave {
+ thisFaveID, err := id.NewRandomULID()
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // we need to create a new fave in the database
+ gtsFave := &gtsmodel.StatusFave{
+ ID: thisFaveID,
+ AccountID: account.ID,
+ TargetAccountID: targetAccount.ID,
+ StatusID: targetStatus.ID,
+ URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID),
+ GTSStatus: targetStatus,
+ GTSTargetAccount: targetAccount,
+ GTSFavingAccount: account,
+ }
+
+ if err := p.db.Put(gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ // return the mastodon representation of the target status
+ mastoStatus, err := p.tc.StatusToMasto(targetStatus, 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/synchronous/status/favedby.go b/internal/processing/synchronous/status/favedby.go
new file mode 100644
index 000000000..bda47d581
--- /dev/null
+++ b/internal/processing/synchronous/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 := &gtsmodel.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 := &gtsmodel.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, 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/synchronous/status/get.go b/internal/processing/synchronous/status/get.go
new file mode 100644
index 000000000..7dbbb4e7d
--- /dev/null
+++ b/internal/processing/synchronous/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 := &gtsmodel.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 := &gtsmodel.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, 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 = &gtsmodel.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/synchronous/status/status.go b/internal/processing/synchronous/status/status.go
new file mode 100644
index 000000000..5dd26a2f0
--- /dev/null
+++ b/internal/processing/synchronous/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/synchronous/status/unfave.go b/internal/processing/synchronous/status/unfave.go
new file mode 100644
index 000000000..54cbbf509
--- /dev/null
+++ b/internal/processing/synchronous/status/unfave.go
@@ -0,0 +1,92 @@
+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 := &gtsmodel.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 := &gtsmodel.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, 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 := &gtsmodel.StatusFave{}
+ err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave)
+ if err == nil {
+ // we have a fave
+ toUnfave = true
+ }
+ if err != nil {
+ // something went wrong in the db finding the fave
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
+ }
+ // we just don't have a fave
+ toUnfave = false
+ }
+
+ if toUnfave {
+ // we had a fave, so take some action to get rid of it
+ if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
+ }
+
+ // send it back to the processor for async processing
+ p.fromClientAPI <- gtsmodel.FromClientAPI{
+ APObjectType: gtsmodel.ActivityStreamsLike,
+ APActivityType: gtsmodel.ActivityStreamsUndo,
+ GTSModel: gtsFave,
+ OriginAccount: account,
+ TargetAccount: targetAccount,
+ }
+ }
+
+ // return the status (whatever its state) back to the caller
+ var boostOfStatus *gtsmodel.Status
+ if targetStatus.BoostOfID != "" {
+ boostOfStatus = &gtsmodel.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/synchronous/status/util.go b/internal/processing/synchronous/status/util.go
new file mode 100644
index 000000000..0a023eab6
--- /dev/null
+++ b/internal/processing/synchronous/status/util.go
@@ -0,0 +1,269 @@
+package status
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
+ // by default all flags are set to true
+ gtsAdvancedVis := &gtsmodel.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 := &gtsmodel.Status{}
+ repliedAccount := &gtsmodel.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 := &gtsmodel.MediaAttachment{}
+ if err := p.db.GetByID(mediaID, a); err != nil {
+ return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
+ }
+ // check they belong to the requesting account id
+ if a.AccountID != thisAccountID {
+ return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
+ }
+ // check they're not already used in a status
+ if a.StatusID != "" || a.ScheduledStatusID != "" {
+ return fmt.Errorf("media with id %s is already attached to a status", mediaID)
+ }
+ gtsMediaAttachments = append(gtsMediaAttachments, a)
+ attachments = append(attachments, a.ID)
+ }
+ status.GTSMediaAttachments = gtsMediaAttachments
+ status.Attachments = attachments
+ return nil
+}
+
+func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
+ if form.Language != "" {
+ status.Language = form.Language
+ } else {
+ status.Language = accountDefaultLanguage
+ }
+ if status.Language == "" {
+ return errors.New("no language given either in status create form or account default")
+ }
+ return nil
+}
+
+func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ menchies := []string{}
+ gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating mentions from status: %s", err)
+ }
+ for _, menchie := range gtsMenchies {
+ menchieID, err := id.NewRandomULID()
+ if err != nil {
+ return err
+ }
+ menchie.ID = menchieID
+
+ if err := p.db.Put(menchie); err != nil {
+ return fmt.Errorf("error putting mentions in db: %s", err)
+ }
+ menchies = append(menchies, menchie.ID)
+ }
+ // add full populated gts menchies to the status for passing them around conveniently
+ status.GTSMentions = gtsMenchies
+ // add just the ids of the mentioned accounts to the status for putting in the db
+ status.Mentions = menchies
+ return nil
+}
+
+func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ tags := []string{}
+ gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating hashtags from status: %s", err)
+ }
+ for _, tag := range gtsTags {
+ if err := p.db.Upsert(tag, "name"); err != nil {
+ return fmt.Errorf("error putting tags in db: %s", err)
+ }
+ tags = append(tags, tag.ID)
+ }
+ // add full populated gts tags to the status for passing them around conveniently
+ status.GTSTags = gtsTags
+ // add just the ids of the used tags to the status for putting in the db
+ status.Tags = tags
+ return nil
+}
+
+func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ emojis := []string{}
+ gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
+ if err != nil {
+ return fmt.Errorf("error generating emojis from status: %s", err)
+ }
+ for _, e := range gtsEmojis {
+ emojis = append(emojis, e.ID)
+ }
+ // add full populated gts emojis to the status for passing them around conveniently
+ status.GTSEmojis = gtsEmojis
+ // add just the ids of the used emojis to the status for putting in the db
+ status.Emojis = emojis
+ return nil
+}
+
+func (p *processor) processContent(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ if form.Status == "" {
+ status.Content = ""
+ return nil
+ }
+
+ // surround the whole status in '<p>'
+ content := fmt.Sprintf(`<p>%s</p>`, form.Status)
+
+ // format mentions nicely
+ for _, menchie := range status.GTSMentions {
+ targetAccount := &gtsmodel.Account{}
+ if err := p.db.GetByID(menchie.TargetAccountID, targetAccount); err == nil {
+ mentionContent := fmt.Sprintf(`<span class="h-card"><a href="%s" class="u-url mention">@<span>%s</span></a></span>`, targetAccount.URL, targetAccount.Username)
+ content = strings.ReplaceAll(content, menchie.NameString, mentionContent)
+ }
+ }
+
+ // format tags nicely
+ for _, tag := range status.GTSTags {
+ tagContent := fmt.Sprintf(`<a href="%s" class="mention hashtag" rel="tag">#<span>%s</span></a>`, tag.URL, tag.Name)
+ content = strings.ReplaceAll(content, fmt.Sprintf("#%s", tag.Name), tagContent)
+ }
+
+ // replace newlines with breaks
+ content = strings.ReplaceAll(content, "\n", "<br />")
+
+ status.Content = content
+ return nil
+}
diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go
index 7de2d63a9..80e63317f 100644
--- a/internal/processing/timeline.go
+++ b/internal/processing/timeline.go
@@ -20,45 +20,70 @@ package processing
import (
"fmt"
+ "net/url"
+ "sync"
+ "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) {
- statuses, err := p.db.GetHomeTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
- if err != nil {
- return nil, NewErrorInternalError(err)
+func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
+ resp := &apimodel.StatusTimelineResponse{
+ Statuses: []*apimodel.Status{},
}
- s, err := p.filterStatuses(authed, statuses)
+ apiStatuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil {
- return nil, NewErrorInternalError(err)
+ return nil, gtserror.NewErrorInternalError(err)
}
+ resp.Statuses = apiStatuses
+
+ // prepare the next and previous links
+ if len(apiStatuses) != 0 {
+ nextLink := &url.URL{
+ Scheme: p.config.Protocol,
+ Host: p.config.Host,
+ Path: "/api/v1/timelines/home",
+ RawPath: url.PathEscape("api/v1/timelines/home"),
+ RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, apiStatuses[len(apiStatuses)-1].ID),
+ }
+ next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
- return s, nil
+ prevLink := &url.URL{
+ Scheme: p.config.Protocol,
+ Host: p.config.Host,
+ Path: "/api/v1/timelines/home",
+ RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, apiStatuses[0].ID),
+ }
+ prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String())
+ resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev)
+ }
+
+ return resp, 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
}
-func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]apimodel.Status, error) {
+func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
l := p.log.WithField("func", "filterStatuses")
- apiStatuses := []apimodel.Status{}
+ apiStatuses := []*apimodel.Status{}
for _, s := range statuses {
targetAccount := &gtsmodel.Account{}
if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
@@ -66,7 +91,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,9 +100,9 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
continue
}
- visible, err := p.db.StatusVisible(s, targetAccount, authed.Account, relevantAccounts)
+ visible, err := p.db.StatusVisible(s, 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
@@ -91,7 +116,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,9 +124,9 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
continue
}
- boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
+ boostedVisible, err := p.db.StatusVisible(bs, 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 {
@@ -115,8 +140,113 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
continue
}
- apiStatuses = append(apiStatuses, *apiStatus)
+ apiStatuses = append(apiStatuses, apiStatus)
}
return apiStatuses, nil
}
+
+func (p *processor) initTimelines() error {
+ // get all local accounts (ie., domain = nil) that aren't suspended (suspended_at = nil)
+ localAccounts := []*gtsmodel.Account{}
+ where := []db.Where{
+ {
+ Key: "domain", Value: nil,
+ },
+ {
+ Key: "suspended_at", Value: nil,
+ },
+ }
+ if err := p.db.GetWhere(where, &localAccounts); err != nil {
+ if _, ok := err.(db.ErrNoEntries); ok {
+ return nil
+ }
+ return fmt.Errorf("initTimelines: db error initializing timelines: %s", err)
+ }
+
+ // we want to wait until all timelines are populated so created a waitgroup here
+ wg := &sync.WaitGroup{}
+ wg.Add(len(localAccounts))
+
+ for _, localAccount := range localAccounts {
+ // to save time we can populate the timelines asynchronously
+ // this will go heavy on the database, but since we're not actually serving yet it doesn't really matter
+ go p.initTimelineFor(localAccount, wg)
+ }
+
+ // wait for all timelines to be populated before we exit
+ wg.Wait()
+ return nil
+}
+
+func (p *processor) initTimelineFor(account *gtsmodel.Account, wg *sync.WaitGroup) {
+ defer wg.Done()
+
+ l := p.log.WithFields(logrus.Fields{
+ "func": "initTimelineFor",
+ "accountID": account.ID,
+ })
+
+ desiredIndexLength := p.timelineManager.GetDesiredIndexLength()
+
+ statuses, err := p.db.GetStatusesWhereFollowing(account.ID, "", "", "", desiredIndexLength, false)
+ if err != nil {
+ if _, ok := err.(db.ErrNoEntries); !ok {
+ l.Error(fmt.Errorf("initTimelineFor: error getting statuses: %s", err))
+ }
+ return
+ }
+ p.indexAndIngest(statuses, account, desiredIndexLength)
+
+ lengthNow := p.timelineManager.GetIndexedLength(account.ID)
+ if lengthNow < desiredIndexLength {
+ // try and get more posts from the last ID onwards
+ rearmostStatusID, err := p.timelineManager.GetOldestIndexedID(account.ID)
+ if err != nil {
+ l.Error(fmt.Errorf("initTimelineFor: error getting id of rearmost status: %s", err))
+ return
+ }
+
+ if rearmostStatusID != "" {
+ moreStatuses, err := p.db.GetStatusesWhereFollowing(account.ID, rearmostStatusID, "", "", desiredIndexLength/2, false)
+ if err != nil {
+ l.Error(fmt.Errorf("initTimelineFor: error getting more statuses: %s", err))
+ return
+ }
+ p.indexAndIngest(moreStatuses, account, desiredIndexLength)
+ }
+ }
+
+ l.Debugf("prepared timeline of length %d for account %s", lengthNow, account.ID)
+}
+
+func (p *processor) indexAndIngest(statuses []*gtsmodel.Status, timelineAccount *gtsmodel.Account, desiredIndexLength int) {
+ l := p.log.WithFields(logrus.Fields{
+ "func": "indexAndIngest",
+ "accountID": timelineAccount.ID,
+ })
+
+ for _, s := range statuses {
+ relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
+ if err != nil {
+ l.Error(fmt.Errorf("initTimelineFor: error getting relevant accounts from status %s: %s", s.ID, err))
+ continue
+ }
+ visible, err := p.db.StatusVisible(s, timelineAccount, relevantAccounts)
+ if err != nil {
+ l.Error(fmt.Errorf("initTimelineFor: error checking visibility of status %s: %s", s.ID, err))
+ continue
+ }
+ if visible {
+ if err := p.timelineManager.Ingest(s, timelineAccount.ID); err != nil {
+ l.Error(fmt.Errorf("initTimelineFor: error ingesting status %s: %s", s.ID, err))
+ continue
+ }
+
+ // check if we have enough posts now and return if we do
+ if p.timelineManager.GetIndexedLength(timelineAccount.ID) >= desiredIndexLength {
+ return
+ }
+ }
+ }
+}
diff --git a/internal/processing/util.go b/internal/processing/util.go
index af62afbad..6474100c1 100644
--- a/internal/processing/util.go
+++ b/internal/processing/util.go
@@ -25,233 +25,11 @@ import (
"io"
"mime/multipart"
- 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/media"
"github.com/superseriousbusiness/gotosocial/internal/transport"
- "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 := &gtsmodel.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 := &gtsmodel.Status{}
- repliedAccount := &gtsmodel.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 := &gtsmodel.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
-}
-
/*
HELPER FUNCTIONS
*/