diff options
author | 2023-10-25 16:04:53 +0200 | |
---|---|---|
committer | 2023-10-25 15:04:53 +0100 | |
commit | c7b6cd7770cad9bfdc23decffa7c4068752dbbbd (patch) | |
tree | 0f039fd34fb0287860fce06ff1c30dedd1882136 /internal/processing | |
parent | [bugfix] allow store smaller PNG image than 261 bytes (#2263) (#2298) (diff) | |
download | gotosocial-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.go | 2 | ||||
-rw-r--r-- | internal/processing/status/bookmark.go | 46 | ||||
-rw-r--r-- | internal/processing/status/boost.go | 4 | ||||
-rw-r--r-- | internal/processing/status/common.go | 103 | ||||
-rw-r--r-- | internal/processing/status/create.go | 37 | ||||
-rw-r--r-- | internal/processing/status/delete.go | 2 | ||||
-rw-r--r-- | internal/processing/status/fave.go | 54 | ||||
-rw-r--r-- | internal/processing/status/get.go | 6 | ||||
-rw-r--r-- | internal/processing/status/mute.go | 146 | ||||
-rw-r--r-- | internal/processing/status/pin.go | 12 | ||||
-rw-r--r-- | internal/processing/status/status.go | 14 | ||||
-rw-r--r-- | internal/processing/status/status_test.go | 5 | ||||
-rw-r--r-- | internal/processing/workers/fromclientapi.go | 5 | ||||
-rw-r--r-- | internal/processing/workers/fromclientapi_test.go | 109 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi.go | 5 | ||||
-rw-r--r-- | internal/processing/workers/surfacenotify.go | 74 | ||||
-rw-r--r-- | internal/processing/workers/surfacetimeline.go | 2 |
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, + >smodel.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, >smodel.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 := >smodel.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 = >smodel.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 = >smodel.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) } |