summaryrefslogtreecommitdiff
path: root/internal/processing
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-10-25 16:04:53 +0200
committerLibravatar GitHub <noreply@github.com>2023-10-25 15:04:53 +0100
commitc7b6cd7770cad9bfdc23decffa7c4068752dbbbd (patch)
tree0f039fd34fb0287860fce06ff1c30dedd1882136 /internal/processing
parent[bugfix] allow store smaller PNG image than 261 bytes (#2263) (#2298) (diff)
downloadgotosocial-c7b6cd7770cad9bfdc23decffa7c4068752dbbbd.tar.xz
[feature] Status thread mute/unmute functionality (#2278)
* add db models + functions for keeping track of threads * give em the old linty testy * create, remove, check mutes * swagger * testerino * test mute/unmute via api * add info log about new index creation * thread + allow muting of any remote statuses that mention a local account * IsStatusThreadMutedBy -> IsThreadMutedByAccount * use common processing functions in status processor * set = NULL * favee! * get rekt darlings, darlings get rekt * testrig please, have mercy muy liege
Diffstat (limited to 'internal/processing')
-rw-r--r--internal/processing/processor.go2
-rw-r--r--internal/processing/status/bookmark.go46
-rw-r--r--internal/processing/status/boost.go4
-rw-r--r--internal/processing/status/common.go103
-rw-r--r--internal/processing/status/create.go37
-rw-r--r--internal/processing/status/delete.go2
-rw-r--r--internal/processing/status/fave.go54
-rw-r--r--internal/processing/status/get.go6
-rw-r--r--internal/processing/status/mute.go146
-rw-r--r--internal/processing/status/pin.go12
-rw-r--r--internal/processing/status/status.go14
-rw-r--r--internal/processing/status/status_test.go5
-rw-r--r--internal/processing/workers/fromclientapi.go5
-rw-r--r--internal/processing/workers/fromclientapi_test.go109
-rw-r--r--internal/processing/workers/fromfediapi.go5
-rw-r--r--internal/processing/workers/surfacenotify.go74
-rw-r--r--internal/processing/workers/surfacetimeline.go2
17 files changed, 452 insertions, 174 deletions
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index a24683e69..47f14a686 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -164,7 +164,7 @@ func NewProcessor(
processor.report = report.New(state, converter)
processor.timeline = timeline.New(state, converter, filter)
processor.search = search.New(state, federator, converter, filter)
- processor.status = status.New(state, federator, converter, filter, parseMentionFunc)
+ processor.status = status.New(&commonProcessor, state, federator, converter, filter, parseMentionFunc)
processor.stream = streamProcessor
processor.user = user.New(state, emailSender)
diff --git a/internal/processing/status/bookmark.go b/internal/processing/status/bookmark.go
index 64e3fc1fd..634529ba4 100644
--- a/internal/processing/status/bookmark.go
+++ b/internal/processing/status/bookmark.go
@@ -29,16 +29,31 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
)
+func (p *Processor) getBookmarkableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
+ if errWithCode != nil {
+ return nil, "", errWithCode
+ }
+
+ bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
+ return nil, "", gtserror.NewErrorInternalError(err)
+ }
+
+ return targetStatus, bookmarkID, nil
+}
+
// 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, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
+ targetStatus, existingBookmarkID, errWithCode := p.getBookmarkableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
if existingBookmarkID != "" {
// Status is already bookmarked.
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// Create and store a new bookmark.
@@ -57,24 +72,24 @@ func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmo
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
+ if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// 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, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
+ targetStatus, existingBookmarkID, errWithCode := p.getBookmarkableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
if existingBookmarkID == "" {
// Status isn't bookmarked.
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// We have a bookmark to remove.
@@ -83,25 +98,10 @@ func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmo
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
+ if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
-}
-
-func (p *Processor) getBookmarkTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
- targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
- if errWithCode != nil {
- return nil, "", errWithCode
- }
-
- bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
- return nil, "", gtserror.NewErrorInternalError(err)
- }
-
- return targetStatus, bookmarkID, nil
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go
index d4bdc3f43..76a0a75bc 100644
--- a/internal/processing/status/boost.go
+++ b/internal/processing/status/boost.go
@@ -85,7 +85,7 @@ func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel
TargetAccount: targetStatus.Account,
})
- return p.apiStatus(ctx, boostWrapperStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, boostWrapperStatus)
}
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
@@ -129,7 +129,7 @@ func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel
})
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
diff --git a/internal/processing/status/common.go b/internal/processing/status/common.go
deleted file mode 100644
index 71eef70a1..000000000
--- a/internal/processing/status/common.go
+++ /dev/null
@@ -1,103 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// 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"
- "fmt"
-
- "codeberg.org/gruf/go-kv"
- 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/log"
-)
-
-func (p *Processor) apiStatus(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, gtserror.WithCode) {
- apiStatus, err := p.converter.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
- if err != nil {
- err = gtserror.Newf("error converting status %s to frontend representation: %w", targetStatus.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return apiStatus, nil
-}
-
-func (p *Processor) getVisibleStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
- targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
- if err != nil {
- err = fmt.Errorf("getVisibleStatus: db error fetching status %s: %w", targetStatusID, err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- if requestingAccount != nil {
- // Ensure the status is up-to-date.
- p.federator.RefreshStatusAsync(ctx,
- requestingAccount.Username,
- targetStatus,
- nil,
- false,
- )
- }
-
- visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus)
- if err != nil {
- err = fmt.Errorf("getVisibleStatus: error seeing if status %s is visible: %w", targetStatus.ID, err)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- if !visible {
- err = fmt.Errorf("getVisibleStatus: status %s is not visible to requesting account", targetStatusID)
- return nil, gtserror.NewErrorNotFound(err)
- }
-
- return targetStatus, nil
-}
-
-// invalidateStatus is a shortcut function for invalidating the prepared/cached
-// representation one status in the home timeline and all list timelines of the
-// given accountID. It should only be called in cases where a status update
-// does *not* need to be passed into the processor via the worker queue, since
-// such invalidation will, in that case, be handled by the processor instead.
-func (p *Processor) invalidateStatus(ctx context.Context, accountID string, statusID string) error {
- // Get lists first + bail if this fails.
- lists, err := p.state.DB.GetListsForAccountID(ctx, accountID)
- if err != nil {
- return gtserror.Newf("db error getting lists for account %s: %w", accountID, err)
- }
-
- l := log.WithContext(ctx).WithFields(kv.Fields{
- {"accountID", accountID},
- {"statusID", statusID},
- }...)
-
- // Unprepare item from home + list timelines, just log
- // if something goes wrong since this is not a showstopper.
-
- if err := p.state.Timelines.Home.UnprepareItem(ctx, accountID, statusID); err != nil {
- l.Errorf("error unpreparing item from home timeline: %v", err)
- }
-
- for _, list := range lists {
- if err := p.state.Timelines.List.UnprepareItem(ctx, list.ID, statusID); err != nil {
- l.Errorf("error unpreparing item from list timeline %s: %v", list.ID, err)
- }
- }
-
- return nil
-}
diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go
index ee4466b1b..40b3f2df2 100644
--- a/internal/processing/status/create.go
+++ b/internal/processing/status/create.go
@@ -70,6 +70,10 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
return nil, errWithCode
}
+ if errWithCode := p.processThreadID(ctx, status); errWithCode != nil {
+ return nil, errWithCode
+ }
+
if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil {
return nil, errWithCode
}
@@ -99,7 +103,7 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
OriginAccount: requestingAccount,
})
- return p.apiStatus(ctx, status, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, status)
}
func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
@@ -141,12 +145,43 @@ func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.Advance
// Set status fields from inReplyTo.
status.InReplyToID = inReplyTo.ID
+ status.InReplyTo = inReplyTo
status.InReplyToURI = inReplyTo.URI
status.InReplyToAccountID = inReplyTo.AccountID
return nil
}
+func (p *Processor) processThreadID(ctx context.Context, status *gtsmodel.Status) gtserror.WithCode {
+ // Status takes the thread ID
+ // of whatever it replies to.
+ if status.InReplyTo != nil {
+ status.ThreadID = status.InReplyTo.ThreadID
+ return nil
+ }
+
+ // Status doesn't reply to anything,
+ // so it's a new local top-level status
+ // and therefore needs a thread ID.
+ threadID := id.NewULID()
+
+ if err := p.state.DB.PutThread(
+ ctx,
+ &gtsmodel.Thread{
+ ID: threadID,
+ },
+ ); err != nil {
+ err := gtserror.Newf("error inserting new thread in db: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Future replies to this status
+ // (if any) will inherit this thread ID.
+ status.ThreadID = threadID
+
+ 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
diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go
index 5549e0329..261086bdb 100644
--- a/internal/processing/status/delete.go
+++ b/internal/processing/status/delete.go
@@ -45,7 +45,7 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
}
// Parse the status to API model BEFORE deleting it.
- apiStatus, errWithCode := p.apiStatus(ctx, targetStatus, requestingAccount)
+ apiStatus, errWithCode := p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
if errWithCode != nil {
return nil, errWithCode
}
diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go
index e2bf03594..a16fb6620 100644
--- a/internal/processing/status/fave.go
+++ b/internal/processing/status/fave.go
@@ -33,16 +33,36 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/uris"
)
+func (p *Processor) getFaveableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
+ if errWithCode != nil {
+ return nil, nil, errWithCode
+ }
+
+ if !*targetStatus.Likeable {
+ err := errors.New("status is not faveable")
+ return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
+ }
+
+ fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
+ return nil, nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return targetStatus, fave, nil
+}
+
// FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists).
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
+ targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
if existingFave != nil {
// Status is already faveed.
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// Create and store a new fave
@@ -72,19 +92,19 @@ func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.
TargetAccount: targetStatus.Account,
})
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
- targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
+ targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
if existingFave == nil {
// Status isn't faveed.
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// We have a fave to remove.
@@ -102,12 +122,12 @@ func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.
TargetAccount: targetStatus.Account,
})
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// 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, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -145,23 +165,3 @@ func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Acc
return apiAccounts, nil
}
-
-func (p *Processor) getFaveTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
- targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
- if errWithCode != nil {
- return nil, nil, errWithCode
- }
-
- if !*targetStatus.Likeable {
- err := errors.New("status is not faveable")
- return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
- }
-
- fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
- return nil, nil, gtserror.NewErrorInternalError(err)
- }
-
- return targetStatus, fave, nil
-}
diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go
index cf79b96a0..8c939a61e 100644
--- a/internal/processing/status/get.go
+++ b/internal/processing/status/get.go
@@ -28,17 +28,17 @@ import (
// 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, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// 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, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
diff --git a/internal/processing/status/mute.go b/internal/processing/status/mute.go
new file mode 100644
index 000000000..1663ee0bc
--- /dev/null
+++ b/internal/processing/status/mute.go
@@ -0,0 +1,146 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// 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"
+
+ 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"
+)
+
+// getMuteableStatus fetches targetStatusID status and
+// ensures that requestingAccount can mute or unmute it.
+//
+// It checks:
+// - Status exists and is visible to requester.
+// - Status belongs to or mentions requesting account.
+// - Status is not a boost.
+// - Status has a thread ID.
+func (p *Processor) getMuteableStatus(
+ ctx context.Context,
+ requestingAccount *gtsmodel.Account,
+ targetStatusID string,
+) (*gtsmodel.Status, gtserror.WithCode) {
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ if !targetStatus.BelongsToAccount(requestingAccount.ID) &&
+ !targetStatus.MentionsAccount(requestingAccount.ID) {
+ err := gtserror.Newf("status %s does not belong to or mention account %s", targetStatusID, requestingAccount.ID)
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if targetStatus.BoostOfID != "" {
+ err := gtserror.New("cannot mute or unmute boosts")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ if targetStatus.ThreadID == "" {
+ err := gtserror.New("cannot mute or unmute status with no threadID")
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ return targetStatus, nil
+}
+
+func (p *Processor) MuteCreate(
+ ctx context.Context,
+ requestingAccount *gtsmodel.Account,
+ targetStatusID string,
+) (*apimodel.Status, gtserror.WithCode) {
+ targetStatus, errWithCode := p.getMuteableStatus(ctx, requestingAccount, targetStatusID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ var (
+ threadID = targetStatus.ThreadID
+ accountID = requestingAccount.ID
+ )
+
+ // Check if mute already exists for this thread ID.
+ threadMute, err := p.state.DB.GetThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ // Real db error.
+ err := gtserror.Newf("db error fetching mute of thread %s for account %s", threadID, accountID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if threadMute != nil {
+ // Thread mute already exists.
+ // Our job here is done ("but you didn't do anything!").
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
+ }
+
+ // Gotta create a mute.
+ if err := p.state.DB.PutThreadMute(ctx, &gtsmodel.ThreadMute{
+ ID: id.NewULID(),
+ ThreadID: threadID,
+ AccountID: accountID,
+ }); err != nil {
+ err := gtserror.Newf("db error putting mute of thread %s for account %s", threadID, accountID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
+}
+
+func (p *Processor) MuteRemove(
+ ctx context.Context,
+ requestingAccount *gtsmodel.Account,
+ targetStatusID string,
+) (*apimodel.Status, gtserror.WithCode) {
+ targetStatus, errWithCode := p.getMuteableStatus(ctx, requestingAccount, targetStatusID)
+ if errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ var (
+ threadID = targetStatus.ThreadID
+ accountID = requestingAccount.ID
+ )
+
+ // Check if mute exists for this thread ID.
+ threadMute, err := p.state.DB.GetThreadMutedByAccount(ctx, threadID, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ // Real db error.
+ err := gtserror.Newf("db error fetching mute of thread %s for account %s", threadID, accountID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if threadMute == nil {
+ // Thread mute doesn't exist.
+ // Our job here is done ("but you didn't do anything!").
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
+ }
+
+ // Gotta remove the mute.
+ if err := p.state.DB.DeleteThreadMute(ctx, threadMute.ID); err != nil {
+ err := gtserror.Newf("db error deleting mute of thread %s for account %s", threadID, accountID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
+}
diff --git a/internal/processing/status/pin.go b/internal/processing/status/pin.go
index c5981b699..b31288a64 100644
--- a/internal/processing/status/pin.go
+++ b/internal/processing/status/pin.go
@@ -39,7 +39,7 @@ const allowedPinnedCount = 10
// - Status is public, unlisted, or followers-only.
// - Status is not a boost.
func (p *Processor) getPinnableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
- targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
+ targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
if errWithCode != nil {
return nil, errWithCode
}
@@ -99,12 +99,12 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
+ if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
@@ -125,7 +125,7 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
}
if targetStatus.PinnedAt.IsZero() {
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
targetStatus.PinnedAt = time.Time{}
@@ -134,10 +134,10 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
return nil, gtserror.NewErrorInternalError(err)
}
- if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
+ if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
err = gtserror.Newf("error invalidating status from timelines: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
- return p.apiStatus(ctx, targetStatus, requestingAccount)
+ return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
}
diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go
index 28ea64542..b45b1651e 100644
--- a/internal/processing/status/status.go
+++ b/internal/processing/status/status.go
@@ -20,6 +20,7 @@ package status
import (
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@@ -27,6 +28,9 @@ import (
)
type Processor struct {
+ // common processor logic
+ c *common.Processor
+
state *state.State
federator *federation.Federator
converter *typeutils.Converter
@@ -36,8 +40,16 @@ type Processor struct {
}
// New returns a new status processor.
-func New(state *state.State, federator *federation.Federator, converter *typeutils.Converter, filter *visibility.Filter, parseMention gtsmodel.ParseMentionFunc) Processor {
+func New(
+ common *common.Processor,
+ state *state.State,
+ federator *federation.Federator,
+ converter *typeutils.Converter,
+ filter *visibility.Filter,
+ parseMention gtsmodel.ParseMentionFunc,
+) Processor {
return Processor{
+ c: common,
state: state,
federator: federator,
converter: converter,
diff --git a/internal/processing/status/status_test.go b/internal/processing/status/status_test.go
index 0507df484..22486ecf2 100644
--- a/internal/processing/status/status_test.go
+++ b/internal/processing/status/status_test.go
@@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/processing"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
@@ -94,7 +95,9 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.typeConverter,
)
- suite.status = status.New(&suite.state, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(suite.db, suite.federator))
+ common := common.New(&suite.state, suite.typeConverter, suite.federator, filter)
+
+ suite.status = status.New(&common, &suite.state, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(suite.db, suite.federator))
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index ff316b1f4..789145226 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -260,6 +260,11 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg messages.FromClientAPI)
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
}
+ // Ensure fave populated.
+ if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
+ return gtserror.Newf("error populating status fave: %w", err)
+ }
+
if err := p.surface.notifyFave(ctx, fave); err != nil {
return gtserror.Newf("error notifying fave: %w", err)
}
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index e5a098c31..05526f437 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -75,6 +75,7 @@ func (suite *FromClientAPITestSuite) newStatus(
newStatus.InReplyToAccountID = replyToStatus.AccountID
newStatus.InReplyToID = replyToStatus.ID
newStatus.InReplyToURI = replyToStatus.URI
+ newStatus.ThreadID = replyToStatus.ThreadID
// Mention the replied-to account.
mention := &gtsmodel.Mention{
@@ -324,6 +325,114 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
)
}
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+
+ // Admin account posts a reply to zork.
+ // Normally zork would get a notification
+ // for this, but zork mutes this thread.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ suite.testStatuses["local_account_1_status_1"],
+ nil,
+ )
+ threadMute = &gtsmodel.ThreadMute{
+ ID: "01HD3KRMBB1M85QRWHD912QWRE",
+ ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
+ AccountID: receivingAccount.ID,
+ }
+ )
+
+ // Store the thread mute before processing new status.
+ if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Ensure no notification received.
+ notif, err := suite.db.GetNotification(
+ ctx,
+ gtsmodel.NotificationMention,
+ receivingAccount.ID,
+ postingAccount.ID,
+ status.ID,
+ )
+
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Nil(notif)
+}
+
+func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
+ var (
+ ctx = context.Background()
+ postingAccount = suite.testAccounts["admin_account"]
+ receivingAccount = suite.testAccounts["local_account_1"]
+
+ // Admin account boosts a status by zork.
+ // Normally zork would get a notification
+ // for this, but zork mutes this thread.
+ status = suite.newStatus(
+ ctx,
+ postingAccount,
+ gtsmodel.VisibilityPublic,
+ nil,
+ suite.testStatuses["local_account_1_status_1"],
+ )
+ threadMute = &gtsmodel.ThreadMute{
+ ID: "01HD3KRMBB1M85QRWHD912QWRE",
+ ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
+ AccountID: receivingAccount.ID,
+ }
+ )
+
+ // Store the thread mute before processing new status.
+ if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Process the new status.
+ if err := suite.processor.Workers().ProcessFromClientAPI(
+ ctx,
+ messages.FromClientAPI{
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: status,
+ OriginAccount: postingAccount,
+ },
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Ensure no notification received.
+ notif, err := suite.db.GetNotification(
+ ctx,
+ gtsmodel.NotificationReblog,
+ receivingAccount.ID,
+ postingAccount.ID,
+ status.ID,
+ )
+
+ suite.ErrorIs(err, db.ErrNoEntries)
+ suite.Nil(notif)
+}
+
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 598480cfb..f57235bf1 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -315,6 +315,11 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) err
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
}
+ // Ensure fave populated.
+ if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
+ return gtserror.Newf("error populating status fave: %w", err)
+ }
+
if err := p.surface.notifyFave(ctx, fave); err != nil {
return gtserror.Newf("error notifying fave: %w", err)
}
diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go
index 5a4f77a64..b99fa3ad3 100644
--- a/internal/processing/workers/surfacenotify.go
+++ b/internal/processing/workers/surfacenotify.go
@@ -28,15 +28,39 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/id"
)
-// notifyMentions notifies each targeted account in
-// the given mentions that they have a new mention.
+// notifyMentions iterates through mentions on the
+// given status, and notifies each mentioned account
+// that they have a new mention.
func (s *surface) notifyMentions(
ctx context.Context,
- mentions []*gtsmodel.Mention,
+ status *gtsmodel.Status,
) error {
- errs := gtserror.NewMultiError(len(mentions))
+ var (
+ mentions = status.Mentions
+ errs = gtserror.NewMultiError(len(mentions))
+ )
for _, mention := range mentions {
+ // Ensure thread not muted
+ // by mentioned account.
+ muted, err := s.state.DB.IsThreadMutedByAccount(
+ ctx,
+ status.ThreadID,
+ mention.TargetAccountID,
+ )
+
+ if err != nil {
+ errs.Append(err)
+ continue
+ }
+
+ if muted {
+ // This mentioned account
+ // has muted the thread.
+ // Don't pester them.
+ continue
+ }
+
if err := s.notify(
ctx,
gtsmodel.NotificationMention,
@@ -114,6 +138,24 @@ func (s *surface) notifyFave(
return nil
}
+ // Ensure favee hasn't
+ // muted the thread.
+ muted, err := s.state.DB.IsThreadMutedByAccount(
+ ctx,
+ fave.Status.ThreadID,
+ fave.TargetAccountID,
+ )
+
+ if err != nil {
+ return err
+ }
+
+ if muted {
+ // Boostee doesn't want
+ // notifs for this thread.
+ return nil
+ }
+
return s.notify(
ctx,
gtsmodel.NotificationFave,
@@ -134,11 +176,35 @@ func (s *surface) notifyAnnounce(
return nil
}
+ if status.BoostOf == nil {
+ // No boosted status
+ // set, nothing to do.
+ return nil
+ }
+
if status.BoostOfAccountID == status.AccountID {
// Self-boost, nothing to do.
return nil
}
+ // Ensure boostee hasn't
+ // muted the thread.
+ muted, err := s.state.DB.IsThreadMutedByAccount(
+ ctx,
+ status.BoostOf.ThreadID,
+ status.BoostOfAccountID,
+ )
+
+ if err != nil {
+ return err
+ }
+
+ if muted {
+ // Boostee doesn't want
+ // notifs for this thread.
+ return nil
+ }
+
return s.notify(
ctx,
gtsmodel.NotificationReblog,
diff --git a/internal/processing/workers/surfacetimeline.go b/internal/processing/workers/surfacetimeline.go
index a45c83188..15263cf78 100644
--- a/internal/processing/workers/surfacetimeline.go
+++ b/internal/processing/workers/surfacetimeline.go
@@ -67,7 +67,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
}
// Notify each local account that's mentioned by this status.
- if err := s.notifyMentions(ctx, status.Mentions); err != nil {
+ if err := s.notifyMentions(ctx, status); err != nil {
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
}