mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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
This commit is contained in:
@@ -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)
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
}
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
146
internal/processing/status/mute.go
Normal file
146
internal/processing/status/mute.go
Normal file
@@ -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)
|
||||
}
|
@@ -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)
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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")
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user