summaryrefslogtreecommitdiff
path: root/internal/processing/status
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processing/status')
-rw-r--r--internal/processing/status/bookmark.go43
-rw-r--r--internal/processing/status/bookmark_test.go22
-rw-r--r--internal/processing/status/boost.go154
-rw-r--r--internal/processing/status/boost_test.go4
-rw-r--r--internal/processing/status/boostedby.go107
-rw-r--r--internal/processing/status/context.go87
-rw-r--r--internal/processing/status/create.go263
-rw-r--r--internal/processing/status/delete.go3
-rw-r--r--internal/processing/status/fave.go111
-rw-r--r--internal/processing/status/favedby.go76
-rw-r--r--internal/processing/status/get.go62
-rw-r--r--internal/processing/status/status.go46
-rw-r--r--internal/processing/status/unbookmark.go69
-rw-r--r--internal/processing/status/unbookmark_test.go54
-rw-r--r--internal/processing/status/unboost.go103
-rw-r--r--internal/processing/status/unfave.go91
-rw-r--r--internal/processing/status/util.go278
-rw-r--r--internal/processing/status/util_test.go155
18 files changed, 650 insertions, 1078 deletions
diff --git a/internal/processing/status/bookmark.go b/internal/processing/status/bookmark.go
index 3cf64490a..dde31ea7d 100644
--- a/internal/processing/status/bookmark.go
+++ b/internal/processing/status/bookmark.go
@@ -30,7 +30,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
)
-func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
+func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -79,3 +80,43 @@ func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Ac
return apiStatus, nil
}
+
+// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
+func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ if targetStatus.Account == nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
+ }
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ 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"))
+ }
+
+ // first check if the status is actually bookmarked
+ toUnbookmark := false
+ gtsBookmark := &gtsmodel.StatusBookmark{}
+ if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
+ // we have a bookmark for this status
+ toUnbookmark = true
+ }
+
+ if toUnbookmark {
+ if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
+ }
+ }
+
+ // return the api representation of the target status
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return apiStatus, nil
+}
diff --git a/internal/processing/status/bookmark_test.go b/internal/processing/status/bookmark_test.go
index bfb652279..a05e19e8b 100644
--- a/internal/processing/status/bookmark_test.go
+++ b/internal/processing/status/bookmark_test.go
@@ -36,13 +36,33 @@ func (suite *StatusBookmarkTestSuite) TestBookmark() {
bookmarkingAccount1 := suite.testAccounts["local_account_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
- bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
+ bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(bookmark1)
suite.True(bookmark1.Bookmarked)
suite.Equal(targetStatus1.ID, bookmark1.ID)
}
+func (suite *StatusBookmarkTestSuite) TestUnbookmark() {
+ ctx := context.Background()
+
+ // bookmark a status
+ bookmarkingAccount1 := suite.testAccounts["local_account_1"]
+ targetStatus1 := suite.testStatuses["admin_account_status_1"]
+
+ bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID)
+ suite.NoError(err)
+ suite.NotNil(bookmark1)
+ suite.True(bookmark1.Bookmarked)
+ suite.Equal(targetStatus1.ID, bookmark1.ID)
+
+ bookmark2, err := suite.status.BookmarkRemove(ctx, bookmarkingAccount1, targetStatus1.ID)
+ suite.NoError(err)
+ suite.NotNil(bookmark2)
+ suite.False(bookmark2.Bookmarked)
+ suite.Equal(targetStatus1.ID, bookmark1.ID)
+}
+
func TestStatusBookmarkTestSuite(t *testing.T) {
suite.Run(t, new(StatusBookmarkTestSuite))
}
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
index 81456abd7..4dfe17019 100644
--- a/internal/processing/status/boost.go
+++ b/internal/processing/status/boost.go
@@ -25,12 +25,14 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
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/messages"
)
-func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+// BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well.
+func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -93,3 +95,153 @@ func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Accou
return apiStatus, nil
}
+
+// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
+func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ if targetStatus.Account == nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
+ }
+ if !visible {
+ return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
+ }
+
+ // check if we actually have a boost for this status
+ var toUnboost bool
+
+ gtsBoost := &gtsmodel.Status{}
+ where := []db.Where{
+ {
+ Key: "boost_of_id",
+ Value: targetStatusID,
+ },
+ {
+ Key: "account_id",
+ Value: requestingAccount.ID,
+ },
+ }
+ err = p.db.GetWhere(ctx, where, gtsBoost)
+ if err == nil {
+ // we have a boost
+ toUnboost = true
+ }
+
+ if err != nil {
+ // something went wrong in the db finding the boost
+ if err != db.ErrNoEntries {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
+ }
+ // we just don't have a boost
+ toUnboost = false
+ }
+
+ if toUnboost {
+ // pin some stuff onto the boost while we have it out of the db
+ gtsBoost.Account = requestingAccount
+ gtsBoost.BoostOf = targetStatus
+ gtsBoost.BoostOfAccount = targetStatus.Account
+ gtsBoost.BoostOf.Account = targetStatus.Account
+
+ // send it back to the processor for async processing
+ p.clientWorker.Queue(messages.FromClientAPI{
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityUndo,
+ GTSModel: gtsBoost,
+ OriginAccount: requestingAccount,
+ TargetAccount: targetStatus.Account,
+ })
+ }
+
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return apiStatus, nil
+}
+
+// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
+func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err)
+ if !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.NewErrorInternalError(wrapped)
+ }
+ return nil, gtserror.NewErrorNotFound(wrapped)
+ }
+
+ if boostOfID := targetStatus.BoostOfID; boostOfID != "" {
+ // the target status is a boost wrapper, redirect this request to the status it boosts
+ boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID)
+ if err != nil {
+ wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err)
+ if !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.NewErrorInternalError(wrapped)
+ }
+ return nil, gtserror.NewErrorNotFound(wrapped)
+ }
+ targetStatus = boostedStatus
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ if err != nil {
+ err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ if !visible {
+ err = errors.New("BoostedBy: status is not visible")
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus)
+ if err != nil {
+ err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // filter account IDs so the user doesn't see accounts they blocked or which blocked them
+ accountIDs := make([]string, 0, len(statusReblogs))
+ for _, s := range statusReblogs {
+ blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true)
+ if err != nil {
+ err = fmt.Errorf("BoostedBy: error checking blocks: %s", err)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ if !blocked {
+ accountIDs = append(accountIDs, s.AccountID)
+ }
+ }
+
+ // TODO: filter other things here? suspended? muted? silenced?
+
+ // fetch accounts + create their API representations
+ apiAccounts := make([]*apimodel.Account, 0, len(accountIDs))
+ for _, accountID := range accountIDs {
+ account, err := p.db.GetAccountByID(ctx, accountID)
+ if err != nil {
+ wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err)
+ if !errors.Is(err, db.ErrNoEntries) {
+ return nil, gtserror.NewErrorInternalError(wrapped)
+ }
+ return nil, gtserror.NewErrorNotFound(wrapped)
+ }
+
+ apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account)
+ if err != nil {
+ err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ apiAccounts = append(apiAccounts, apiAccount)
+ }
+
+ return apiAccounts, nil
+}
diff --git a/internal/processing/status/boost_test.go b/internal/processing/status/boost_test.go
index 4913ff4d0..1a5596cab 100644
--- a/internal/processing/status/boost_test.go
+++ b/internal/processing/status/boost_test.go
@@ -37,7 +37,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() {
application1 := suite.testApplications["application_1"]
targetStatus1 := suite.testStatuses["admin_account_status_1"]
- boost1, err := suite.status.Boost(ctx, boostingAccount1, application1, targetStatus1.ID)
+ boost1, err := suite.status.BoostCreate(ctx, boostingAccount1, application1, targetStatus1.ID)
suite.NoError(err)
suite.NotNil(boost1)
suite.Equal(targetStatus1.ID, boost1.Reblog.ID)
@@ -47,7 +47,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() {
application2 := suite.testApplications["application_2"]
targetStatus2ID := boost1.ID
- boost2, err := suite.status.Boost(ctx, boostingAccount2, application2, targetStatus2ID)
+ boost2, err := suite.status.BoostCreate(ctx, boostingAccount2, application2, targetStatus2ID)
suite.NoError(err)
suite.NotNil(boost2)
// the boosted status should not be the boost,
diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go
deleted file mode 100644
index 97c4e8634..000000000
--- a/internal/processing/status/boostedby.go
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "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) BoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err)
- if !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorInternalError(wrapped)
- }
- return nil, gtserror.NewErrorNotFound(wrapped)
- }
-
- if boostOfID := targetStatus.BoostOfID; boostOfID != "" {
- // the target status is a boost wrapper, redirect this request to the status it boosts
- boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID)
- if err != nil {
- wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err)
- if !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorInternalError(wrapped)
- }
- return nil, gtserror.NewErrorNotFound(wrapped)
- }
- targetStatus = boostedStatus
- }
-
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- if err != nil {
- err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err)
- return nil, gtserror.NewErrorNotFound(err)
- }
- if !visible {
- err = errors.New("BoostedBy: status is not visible")
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus)
- if err != nil {
- err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- // filter account IDs so the user doesn't see accounts they blocked or which blocked them
- accountIDs := make([]string, 0, len(statusReblogs))
- for _, s := range statusReblogs {
- blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true)
- if err != nil {
- err = fmt.Errorf("BoostedBy: error checking blocks: %s", err)
- return nil, gtserror.NewErrorNotFound(err)
- }
- if !blocked {
- accountIDs = append(accountIDs, s.AccountID)
- }
- }
-
- // TODO: filter other things here? suspended? muted? silenced?
-
- // fetch accounts + create their API representations
- apiAccounts := make([]*apimodel.Account, 0, len(accountIDs))
- for _, accountID := range accountIDs {
- account, err := p.db.GetAccountByID(ctx, accountID)
- if err != nil {
- wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err)
- if !errors.Is(err, db.ErrNoEntries) {
- return nil, gtserror.NewErrorInternalError(wrapped)
- }
- return nil, gtserror.NewErrorNotFound(wrapped)
- }
-
- apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account)
- if err != nil {
- err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- apiAccounts = append(apiAccounts, apiAccount)
- }
-
- return apiAccounts, nil
-}
diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go
deleted file mode 100644
index 8d6f1d6ea..000000000
--- a/internal/processing/status/context.go
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "errors"
- "fmt"
- "sort"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-func (p *processor) Context(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- 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"))
- }
-
- context := &apimodel.Context{
- Ancestors: []apimodel.Status{},
- Descendants: []apimodel.Status{},
- }
-
- parents, err := p.db.GetStatusParents(ctx, targetStatus, false)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, status := range parents {
- if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
- if err == nil {
- context.Ancestors = append(context.Ancestors, *apiStatus)
- }
- }
- }
-
- sort.Slice(context.Ancestors, func(i int, j int) bool {
- return context.Ancestors[i].ID < context.Ancestors[j].ID
- })
-
- children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "")
- if err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- for _, status := range children {
- if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
- if err == nil {
- context.Descendants = append(context.Descendants, *apiStatus)
- }
- }
- }
-
- return context, nil
-}
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index 5bc1629c4..f47c850dd 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -20,20 +20,25 @@ package status
import (
"context"
+ "errors"
"fmt"
"time"
"github.com/superseriousbusiness/gotosocial/internal/ap"
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/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
-func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
+// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
+func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
accountURIs := uris.GenerateURIsForAccount(account.Username)
thisStatusID := id.NewULID()
local := true
@@ -56,23 +61,23 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli
Text: form.Status,
}
- if errWithCode := p.ProcessReplyToID(ctx, form, account.ID, newStatus); errWithCode != nil {
+ if errWithCode := processReplyToID(ctx, p.db, form, account.ID, newStatus); errWithCode != nil {
return nil, errWithCode
}
- if errWithCode := p.ProcessMediaIDs(ctx, form, account.ID, newStatus); errWithCode != nil {
+ if errWithCode := processMediaIDs(ctx, p.db, form, account.ID, newStatus); errWithCode != nil {
return nil, errWithCode
}
- if err := p.ProcessVisibility(ctx, form, account.Privacy, newStatus); err != nil {
+ if err := processVisibility(ctx, form, account.Privacy, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.ProcessLanguage(ctx, form, account.Language, newStatus); err != nil {
+ if err := processLanguage(ctx, form, account.Language, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.ProcessContent(ctx, form, account.ID, newStatus); err != nil {
+ if err := processContent(ctx, p.db, p.formatter, p.parseMention, form, account.ID, newStatus); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -97,3 +102,249 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli
return apiStatus, nil
}
+
+func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
+ 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{}
+
+ if err := dbService.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
+ if err == db.ErrNoEntries {
+ err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
+ return gtserror.NewErrorInternalError(err)
+ }
+ if !*repliedStatus.Replyable {
+ err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
+ return gtserror.NewErrorForbidden(err, err.Error())
+ }
+
+ if err := dbService.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
+ if err == db.ErrNoEntries {
+ err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if blocked, err := dbService.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil {
+ err := fmt.Errorf("db error checking block: %s", err)
+ return gtserror.NewErrorInternalError(err)
+ } else if blocked {
+ err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ status.InReplyToID = repliedStatus.ID
+ status.InReplyToURI = repliedStatus.URI
+ status.InReplyToAccountID = repliedAccount.ID
+
+ return nil
+}
+
+func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
+ if form.MediaIDs == nil {
+ return nil
+ }
+
+ attachments := []*gtsmodel.MediaAttachment{}
+ attachmentIDs := []string{}
+ for _, mediaID := range form.MediaIDs {
+ attachment, err := dbService.GetAttachmentByID(ctx, mediaID)
+ if err != nil {
+ if errors.Is(err, db.ErrNoEntries) {
+ err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+ err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ if attachment.AccountID != thisAccountID {
+ err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
+ err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ minDescriptionChars := config.GetMediaDescriptionMinChars()
+ if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
+ err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ attachments = append(attachments, attachment)
+ attachmentIDs = append(attachmentIDs, attachment.ID)
+ }
+
+ status.Attachments = attachments
+ status.AttachmentIDs = attachmentIDs
+ return nil
+}
+
+func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
+ // by default all flags are set to true
+ federated := true
+ boostable := true
+ replyable := true
+ likeable := true
+
+ // If visibility isn't set on the form, then just take the account default.
+ // If that's also not set, take the default for the whole instance.
+ var vis gtsmodel.Visibility
+ switch {
+ case form.Visibility != "":
+ vis = typeutils.APIVisToVis(form.Visibility)
+ case accountDefaultVis != "":
+ vis = accountDefaultVis
+ default:
+ vis = gtsmodel.VisibilityDefault
+ }
+
+ switch vis {
+ 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 {
+ federated = *form.Federated
+ }
+
+ if form.Boostable != nil {
+ boostable = *form.Boostable
+ }
+
+ if form.Replyable != nil {
+ replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ 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
+ boostable = false
+
+ if form.Federated != nil {
+ federated = *form.Federated
+ }
+
+ if form.Replyable != nil {
+ replyable = *form.Replyable
+ }
+
+ if form.Likeable != nil {
+ likeable = *form.Likeable
+ }
+
+ case gtsmodel.VisibilityDirect:
+ // direct is pretty easy: there's only one possible setting so return it
+ federated = true
+ boostable = false
+ replyable = true
+ likeable = true
+ }
+
+ status.Visibility = vis
+ status.Federated = &federated
+ status.Boostable = &boostable
+ status.Replyable = &replyable
+ status.Likeable = &likeable
+ return nil
+}
+
+func processLanguage(ctx context.Context, 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 processContent(ctx context.Context, dbService db.DB, formatter text.Formatter, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
+ // if there's nothing in the status at all we can just return early
+ if form.Status == "" {
+ status.Content = ""
+ return nil
+ }
+
+ // if format wasn't specified we should try to figure out what format this user prefers
+ if form.Format == "" {
+ acct, err := dbService.GetAccountByID(ctx, accountID)
+ if err != nil {
+ return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
+ }
+
+ switch acct.StatusFormat {
+ case "plain":
+ form.Format = apimodel.StatusFormatPlain
+ case "markdown":
+ form.Format = apimodel.StatusFormatMarkdown
+ default:
+ form.Format = apimodel.StatusFormatDefault
+ }
+ }
+
+ // parse content out of the status depending on what format has been submitted
+ var f text.FormatFunc
+ switch form.Format {
+ case apimodel.StatusFormatPlain:
+ f = formatter.FromPlain
+ case apimodel.StatusFormatMarkdown:
+ f = formatter.FromMarkdown
+ default:
+ return fmt.Errorf("format %s not recognised as a valid status format", form.Format)
+ }
+ formatted := f(ctx, parseMention, accountID, status.ID, form.Status)
+
+ // add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
+ // add just their ids to the status for putting in the db
+ status.Mentions = formatted.Mentions
+ status.MentionIDs = make([]string, 0, len(formatted.Mentions))
+ for _, gtsmention := range formatted.Mentions {
+ status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
+ }
+
+ status.Tags = formatted.Tags
+ status.TagIDs = make([]string, 0, len(formatted.Tags))
+ for _, gtstag := range formatted.Tags {
+ status.TagIDs = append(status.TagIDs, gtstag.ID)
+ }
+
+ status.Emojis = formatted.Emojis
+ status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
+ for _, gtsemoji := range formatted.Emojis {
+ status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
+ }
+
+ spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText)
+ for _, gtsemoji := range spoilerformatted.Emojis {
+ status.Emojis = append(status.Emojis, gtsemoji)
+ status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
+ }
+
+ status.Content = formatted.HTML
+ return nil
+}
diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go
index 0042c043d..d3a03aad6 100644
--- a/internal/processing/status/delete.go
+++ b/internal/processing/status/delete.go
@@ -30,7 +30,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
-func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+// Delete processes the delete of a given status, returning the deleted status if the delete goes through.
+func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
index dd5d338b3..3bcb1835f 100644
--- a/internal/processing/status/fave.go
+++ b/internal/processing/status/fave.go
@@ -33,7 +33,8 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
-func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+// FaveCreate processes the faving of a given status, returning the updated status if the fave goes through.
+func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -98,3 +99,111 @@ func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Accoun
return apiStatus, nil
}
+
+// FaveRemove processes the unfaving of a given status, returning the updated status if the fave goes through.
+func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ if targetStatus.Account == nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ 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(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
+ if err == nil {
+ // we have a fave
+ toUnfave = true
+ }
+ if err != nil {
+ // something went wrong in the db finding the fave
+ if err != db.ErrNoEntries {
+ 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(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.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.clientWorker.Queue(messages.FromClientAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityUndo,
+ GTSModel: gtsFave,
+ OriginAccount: requestingAccount,
+ TargetAccount: targetStatus.Account,
+ })
+ }
+
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+
+ return apiStatus, nil
+}
+
+// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
+func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ if targetStatus.Account == nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ 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"))
+ }
+
+ statusFaves, err := p.db.GetStatusFaves(ctx, 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 _, fave := range statusFaves {
+ blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
+ }
+ if !blocked {
+ filteredAccounts = append(filteredAccounts, fave.Account)
+ }
+ }
+
+ // now we can return the api representation of those accounts
+ apiAccounts := []*apimodel.Account{}
+ for _, acc := range filteredAccounts {
+ apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
+ }
+ apiAccounts = append(apiAccounts, apiAccount)
+ }
+
+ return apiAccounts, nil
+}
diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go
deleted file mode 100644
index 2de4aff56..000000000
--- a/internal/processing/status/favedby.go
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "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(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- 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"))
- }
-
- statusFaves, err := p.db.GetStatusFaves(ctx, 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 _, fave := range statusFaves {
- blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
- }
- if !blocked {
- filteredAccounts = append(filteredAccounts, fave.Account)
- }
- }
-
- // now we can return the api representation of those accounts
- apiAccounts := []*apimodel.Account{}
- for _, acc := range filteredAccounts {
- apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
- apiAccounts = append(apiAccounts, apiAccount)
- }
-
- return apiAccounts, nil
-}
diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go
index c79f0d4d6..edefeb440 100644
--- a/internal/processing/status/get.go
+++ b/internal/processing/status/get.go
@@ -22,13 +22,15 @@ import (
"context"
"errors"
"fmt"
+ "sort"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
+// Get gets the given status, taking account of privacy settings and blocks etc.
+func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
if err != nil {
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
@@ -52,3 +54,61 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account
return apiStatus, nil
}
+
+// ContextGet returns the context (previous and following posts) from the given status ID.
+func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
+ targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
+ }
+ if targetStatus.Account == nil {
+ return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
+ }
+
+ visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
+ 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"))
+ }
+
+ context := &apimodel.Context{
+ Ancestors: []apimodel.Status{},
+ Descendants: []apimodel.Status{},
+ }
+
+ parents, err := p.db.GetStatusParents(ctx, targetStatus, false)
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range parents {
+ if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
+ if err == nil {
+ context.Ancestors = append(context.Ancestors, *apiStatus)
+ }
+ }
+ }
+
+ sort.Slice(context.Ancestors, func(i int, j int) bool {
+ return context.Ancestors[i].ID < context.Ancestors[j].ID
+ })
+
+ children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "")
+ if err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ for _, status := range children {
+ if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v {
+ apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount)
+ if err == nil {
+ context.Descendants = append(context.Descendants, *apiStatus)
+ }
+ }
+ }
+
+ return context, nil
+}
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
index 56b8b23eb..c91fd85d1 100644
--- a/internal/processing/status/status.go
+++ b/internal/processing/status/status.go
@@ -19,12 +19,8 @@
package status
import (
- "context"
-
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
"github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/text"
@@ -32,45 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/visibility"
)
-// Processor wraps a bunch of functions for processing statuses.
-type Processor interface {
- // Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
- Create(ctx context.Context, 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(ctx context.Context, 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(ctx context.Context, 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(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Unboost processes the unboost/unreblog of a given status, returning the status if all is well.
- Unboost(ctx context.Context, 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(ctx context.Context, 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(ctx context.Context, account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
- // Get gets the given status, taking account of privacy settings and blocks etc.
- Get(ctx context.Context, 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(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Context returns the context (previous and following posts) from the given status ID
- Context(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
- // Bookmarks a status
- Bookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
- // Removes a bookmark for a status
- Unbookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
-
- /*
- PROCESSING UTILS
- */
-
- ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error
- ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode
- ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode
- ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error
- ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error
-}
-
-type processor struct {
+type Processor struct {
tc typeutils.TypeConverter
db db.DB
filter visibility.Filter
@@ -81,7 +39,7 @@ type processor struct {
// New returns a new status processor.
func New(db db.DB, tc typeutils.TypeConverter, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], parseMention gtsmodel.ParseMentionFunc) Processor {
- return &processor{
+ return Processor{
tc: tc,
db: db,
filter: visibility.NewFilter(db),
diff --git a/internal/processing/status/unbookmark.go b/internal/processing/status/unbookmark.go
deleted file mode 100644
index 497af0e07..000000000
--- a/internal/processing/status/unbookmark.go
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "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) Unbookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- 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"))
- }
-
- // first check if the status is already bookmarked
- toUnbookmark := false
- gtsBookmark := &gtsmodel.StatusBookmark{}
- if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil {
- // we already have a bookmark for this status
- toUnbookmark = true
- }
-
- if toUnbookmark {
- if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
- }
- }
-
- // return the apidon representation of the target status
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return apiStatus, nil
-}
diff --git a/internal/processing/status/unbookmark_test.go b/internal/processing/status/unbookmark_test.go
deleted file mode 100644
index 1e75bc726..000000000
--- a/internal/processing/status/unbookmark_test.go
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status_test
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/suite"
-)
-
-type StatusUnbookmarkTestSuite struct {
- StatusStandardTestSuite
-}
-
-func (suite *StatusUnbookmarkTestSuite) TestUnbookmark() {
- ctx := context.Background()
-
- // bookmark a status
- bookmarkingAccount1 := suite.testAccounts["local_account_1"]
- targetStatus1 := suite.testStatuses["admin_account_status_1"]
-
- bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
- suite.NoError(err)
- suite.NotNil(bookmark1)
- suite.True(bookmark1.Bookmarked)
- suite.Equal(targetStatus1.ID, bookmark1.ID)
-
- bookmark2, err := suite.status.Unbookmark(ctx, bookmarkingAccount1, targetStatus1.ID)
- suite.NoError(err)
- suite.NotNil(bookmark2)
- suite.False(bookmark2.Bookmarked)
- suite.Equal(targetStatus1.ID, bookmark1.ID)
-}
-
-func TestStatusUnbookmarkTestSuite(t *testing.T) {
- suite.Run(t, new(StatusUnbookmarkTestSuite))
-}
diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go
deleted file mode 100644
index 0513e9e81..000000000
--- a/internal/processing/status/unboost.go
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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/messages"
-)
-
-func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
- }
- if !visible {
- return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
- }
-
- // check if we actually have a boost for this status
- var toUnboost bool
-
- gtsBoost := &gtsmodel.Status{}
- where := []db.Where{
- {
- Key: "boost_of_id",
- Value: targetStatusID,
- },
- {
- Key: "account_id",
- Value: requestingAccount.ID,
- },
- }
- err = p.db.GetWhere(ctx, where, gtsBoost)
- if err == nil {
- // we have a boost
- toUnboost = true
- }
-
- if err != nil {
- // something went wrong in the db finding the boost
- if err != db.ErrNoEntries {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err))
- }
- // we just don't have a boost
- toUnboost = false
- }
-
- if toUnboost {
- // pin some stuff onto the boost while we have it out of the db
- gtsBoost.Account = requestingAccount
- gtsBoost.BoostOf = targetStatus
- gtsBoost.BoostOfAccount = targetStatus.Account
- gtsBoost.BoostOf.Account = targetStatus.Account
-
- // send it back to the processor for async processing
- p.clientWorker.Queue(messages.FromClientAPI{
- APObjectType: ap.ActivityAnnounce,
- APActivityType: ap.ActivityUndo,
- GTSModel: gtsBoost,
- OriginAccount: requestingAccount,
- TargetAccount: targetStatus.Account,
- })
- }
-
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return apiStatus, nil
-}
diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go
deleted file mode 100644
index 809c23884..000000000
--- a/internal/processing/status/unfave.go
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- 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/messages"
-)
-
-func (p *processor) Unfave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
- }
- if targetStatus.Account == nil {
- return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID))
- }
-
- visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount)
- 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(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave)
- if err == nil {
- // we have a fave
- toUnfave = true
- }
- if err != nil {
- // something went wrong in the db finding the fave
- if err != db.ErrNoEntries {
- 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(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.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.clientWorker.Queue(messages.FromClientAPI{
- APObjectType: ap.ActivityLike,
- APActivityType: ap.ActivityUndo,
- GTSModel: gtsFave,
- OriginAccount: requestingAccount,
- TargetAccount: targetStatus.Account,
- })
- }
-
- apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
- if err != nil {
- return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
- }
-
- return apiStatus, nil
-}
diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go
deleted file mode 100644
index 1115219cd..000000000
--- a/internal/processing/status/util.go
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status
-
-import (
- "context"
- "errors"
- "fmt"
-
- 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/text"
-)
-
-func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
- // by default all flags are set to true
- federated := true
- boostable := true
- replyable := true
- likeable := true
-
- // If visibility isn't set on the form, then just take the account default.
- // If that's also not set, take the default for the whole instance.
- var vis gtsmodel.Visibility
- switch {
- case form.Visibility != "":
- vis = p.tc.APIVisToVis(form.Visibility)
- case accountDefaultVis != "":
- vis = accountDefaultVis
- default:
- vis = gtsmodel.VisibilityDefault
- }
-
- switch vis {
- 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 {
- federated = *form.Federated
- }
-
- if form.Boostable != nil {
- boostable = *form.Boostable
- }
-
- if form.Replyable != nil {
- replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- 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
- boostable = false
-
- if form.Federated != nil {
- federated = *form.Federated
- }
-
- if form.Replyable != nil {
- replyable = *form.Replyable
- }
-
- if form.Likeable != nil {
- likeable = *form.Likeable
- }
-
- case gtsmodel.VisibilityDirect:
- // direct is pretty easy: there's only one possible setting so return it
- federated = true
- boostable = false
- replyable = true
- likeable = true
- }
-
- status.Visibility = vis
- status.Federated = &federated
- status.Boostable = &boostable
- status.Replyable = &replyable
- status.Likeable = &likeable
- return nil
-}
-
-func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
- 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{}
-
- if err := p.db.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil {
- if err == db.ErrNoEntries {
- err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err)
- return gtserror.NewErrorInternalError(err)
- }
- if !*repliedStatus.Replyable {
- err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
- return gtserror.NewErrorForbidden(err, err.Error())
- }
-
- if err := p.db.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil {
- if err == db.ErrNoEntries {
- err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err)
- return gtserror.NewErrorInternalError(err)
- }
-
- if blocked, err := p.db.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil {
- err := fmt.Errorf("db error checking block: %s", err)
- return gtserror.NewErrorInternalError(err)
- } else if blocked {
- err := fmt.Errorf("status with id %s not replyable", form.InReplyToID)
- return gtserror.NewErrorNotFound(err)
- }
-
- status.InReplyToID = repliedStatus.ID
- status.InReplyToURI = repliedStatus.URI
- status.InReplyToAccountID = repliedAccount.ID
-
- return nil
-}
-
-func (p *processor) ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
- if form.MediaIDs == nil {
- return nil
- }
-
- attachments := []*gtsmodel.MediaAttachment{}
- attachmentIDs := []string{}
- for _, mediaID := range form.MediaIDs {
- attachment, err := p.db.GetAttachmentByID(ctx, mediaID)
- if err != nil {
- if errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
- err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID)
- return gtserror.NewErrorInternalError(err)
- }
-
- if attachment.AccountID != thisAccountID {
- err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- if attachment.StatusID != "" || attachment.ScheduledStatusID != "" {
- err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- minDescriptionChars := config.GetMediaDescriptionMinChars()
- if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars {
- err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID)
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- attachments = append(attachments, attachment)
- attachmentIDs = append(attachmentIDs, attachment.ID)
- }
-
- status.Attachments = attachments
- status.AttachmentIDs = attachmentIDs
- return nil
-}
-
-func (p *processor) ProcessLanguage(ctx context.Context, 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) ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
- // if there's nothing in the status at all we can just return early
- if form.Status == "" {
- status.Content = ""
- return nil
- }
-
- // if format wasn't specified we should try to figure out what format this user prefers
- if form.Format == "" {
- acct, err := p.db.GetAccountByID(ctx, accountID)
- if err != nil {
- return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err)
- }
-
- switch acct.StatusFormat {
- case "plain":
- form.Format = apimodel.StatusFormatPlain
- case "markdown":
- form.Format = apimodel.StatusFormatMarkdown
- default:
- form.Format = apimodel.StatusFormatDefault
- }
- }
-
- // parse content out of the status depending on what format has been submitted
- var f text.FormatFunc
- switch form.Format {
- case apimodel.StatusFormatPlain:
- f = p.formatter.FromPlain
- case apimodel.StatusFormatMarkdown:
- f = p.formatter.FromMarkdown
- default:
- return fmt.Errorf("format %s not recognised as a valid status format", form.Format)
- }
- formatted := f(ctx, p.parseMention, accountID, status.ID, form.Status)
-
- // add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently
- // add just their ids to the status for putting in the db
- status.Mentions = formatted.Mentions
- status.MentionIDs = make([]string, 0, len(formatted.Mentions))
- for _, gtsmention := range formatted.Mentions {
- status.MentionIDs = append(status.MentionIDs, gtsmention.ID)
- }
-
- status.Tags = formatted.Tags
- status.TagIDs = make([]string, 0, len(formatted.Tags))
- for _, gtstag := range formatted.Tags {
- status.TagIDs = append(status.TagIDs, gtstag.ID)
- }
-
- status.Emojis = formatted.Emojis
- status.EmojiIDs = make([]string, 0, len(formatted.Emojis))
- for _, gtsemoji := range formatted.Emojis {
- status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
- }
-
- spoilerformatted := p.formatter.FromPlainEmojiOnly(ctx, p.parseMention, accountID, status.ID, form.SpoilerText)
- for _, gtsemoji := range spoilerformatted.Emojis {
- status.Emojis = append(status.Emojis, gtsemoji)
- status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID)
- }
-
- status.Content = formatted.HTML
- return nil
-}
diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go
deleted file mode 100644
index acd823188..000000000
--- a/internal/processing/status/util_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 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 status_test
-
-import (
- "context"
- "fmt"
- "testing"
-
- "github.com/stretchr/testify/suite"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
-)
-
-const (
- statusText1 = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\nText"
- statusText1Expected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br>Text</p>"
- statusText2 = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\n#hashTAG"
- status2TextExpected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>hashTAG</span></a></p>"
-)
-
-type UtilTestSuite struct {
- StatusStandardTestSuite
-}
-
-func (suite *UtilTestSuite) TestProcessContent1() {
- /*
- TEST PREPARATION
- */
- // we need to partially process the status first since processContent expects a status with some stuff already set on it
- creatingAccount := suite.testAccounts["local_account_1"]
- mentionedAccount := suite.testAccounts["remote_account_1"]
- form := &apimodel.AdvancedStatusCreateForm{
- StatusCreateRequest: apimodel.StatusCreateRequest{
- Status: statusText1,
- MediaIDs: []string{},
- Poll: nil,
- InReplyToID: "",
- Sensitive: false,
- SpoilerText: "",
- Visibility: apimodel.VisibilityPublic,
- ScheduledAt: "",
- Language: "en",
- Format: apimodel.StatusFormatPlain,
- },
- AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
- Federated: nil,
- Boostable: nil,
- Replyable: nil,
- Likeable: nil,
- },
- }
-
- status := &gtsmodel.Status{
- ID: "01FCTDD78JJMX3K9KPXQ7ZQ8BJ",
- }
-
- /*
- ACTUAL TEST
- */
-
- err := suite.status.ProcessContent(context.Background(), form, creatingAccount.ID, status)
- suite.NoError(err)
- suite.Equal(statusText1Expected, status.Content)
-
- suite.Len(status.Mentions, 1)
- newMention := status.Mentions[0]
- suite.Equal(mentionedAccount.ID, newMention.TargetAccountID)
- suite.Equal(creatingAccount.ID, newMention.OriginAccountID)
- suite.Equal(creatingAccount.URI, newMention.OriginAccountURI)
- suite.Equal(status.ID, newMention.StatusID)
- suite.Equal(fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString)
- suite.Equal(mentionedAccount.URI, newMention.TargetAccountURI)
- suite.Equal(mentionedAccount.URL, newMention.TargetAccountURL)
- suite.NotNil(newMention.OriginAccount)
-
- suite.Len(status.MentionIDs, 1)
- suite.Equal(newMention.ID, status.MentionIDs[0])
-}
-
-func (suite *UtilTestSuite) TestProcessContent2() {
- /*
- TEST PREPARATION
- */
- // we need to partially process the status first since processContent expects a status with some stuff already set on it
- creatingAccount := suite.testAccounts["local_account_1"]
- mentionedAccount := suite.testAccounts["remote_account_1"]
- form := &apimodel.AdvancedStatusCreateForm{
- StatusCreateRequest: apimodel.StatusCreateRequest{
- Status: statusText2,
- MediaIDs: []string{},
- Poll: nil,
- InReplyToID: "",
- Sensitive: false,
- SpoilerText: "",
- Visibility: apimodel.VisibilityPublic,
- ScheduledAt: "",
- Language: "en",
- Format: apimodel.StatusFormatPlain,
- },
- AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{
- Federated: nil,
- Boostable: nil,
- Replyable: nil,
- Likeable: nil,
- },
- }
-
- status := &gtsmodel.Status{
- ID: "01FCTDD78JJMX3K9KPXQ7ZQ8BJ",
- }
-
- /*
- ACTUAL TEST
- */
-
- err := suite.status.ProcessContent(context.Background(), form, creatingAccount.ID, status)
- suite.NoError(err)
-
- suite.Equal(status2TextExpected, status.Content)
-
- suite.Len(status.Mentions, 1)
- newMention := status.Mentions[0]
- suite.Equal(mentionedAccount.ID, newMention.TargetAccountID)
- suite.Equal(creatingAccount.ID, newMention.OriginAccountID)
- suite.Equal(creatingAccount.URI, newMention.OriginAccountURI)
- suite.Equal(status.ID, newMention.StatusID)
- suite.Equal(fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString)
- suite.Equal(mentionedAccount.URI, newMention.TargetAccountURI)
- suite.Equal(mentionedAccount.URL, newMention.TargetAccountURL)
- suite.NotNil(newMention.OriginAccount)
-
- suite.Len(status.MentionIDs, 1)
- suite.Equal(newMention.ID, status.MentionIDs[0])
-}
-
-func TestUtilTestSuite(t *testing.T) {
- suite.Run(t, new(UtilTestSuite))
-}