mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-18 10:27:30 -06:00
[chore] Add interaction filter to complement existing visibility filter (#3111)
* [chore] Add interaction filter to complement existing visibility filter
* pass in ptr to visibility and interaction filters to Processor{} to ensure shared
* use int constants for for match type, cache db calls in filterctx
* function name typo 😇
---------
Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
63fc9b6c3e
commit
c9b6220fef
61 changed files with 1661 additions and 585 deletions
|
|
@ -69,12 +69,6 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Generate an ID for the boost wrapper status.
|
||||
boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating id: %w", err)
|
||||
}
|
||||
|
||||
// Set boost_of_uri again in case the
|
||||
// original URI was an indirect link.
|
||||
boost.BoostOfURI = target.URI
|
||||
|
|
@ -92,6 +86,24 @@ func (d *Dereferencer) EnrichAnnounce(
|
|||
boost.Visibility = target.Visibility
|
||||
boost.Federated = target.Federated
|
||||
|
||||
// Ensure this Announce is permitted by the Announcee.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, nil, boost)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error checking permitted status %s: %w", boost.URI, err)
|
||||
}
|
||||
|
||||
if !permit {
|
||||
// Return a checkable error type that can be ignored.
|
||||
err := gtserror.Newf("dropping unpermitted status: %s", boost.URI)
|
||||
return nil, gtserror.SetNotPermitted(err)
|
||||
}
|
||||
|
||||
// Generate an ID for the boost wrapper status.
|
||||
boost.ID, err = id.NewULIDFromTime(boost.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error generating id: %w", err)
|
||||
}
|
||||
|
||||
// Store the boost wrapper status in database.
|
||||
switch err = d.state.DB.PutStatus(ctx, boost); {
|
||||
case err == nil:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
|
|
@ -83,7 +84,8 @@ type Dereferencer struct {
|
|||
converter *typeutils.Converter
|
||||
transportController transport.Controller
|
||||
mediaManager *media.Manager
|
||||
visibility *visibility.Filter
|
||||
visFilter *visibility.Filter
|
||||
intFilter *interaction.Filter
|
||||
|
||||
// in-progress dereferencing media / emoji
|
||||
derefMedia map[string]*media.ProcessingMedia
|
||||
|
|
@ -102,12 +104,14 @@ type Dereferencer struct {
|
|||
handshakesMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewDereferencer returns a Dereferencer initialized with the given parameters.
|
||||
// NewDereferencer returns a Dereferencer
|
||||
// initialized with the given parameters.
|
||||
func NewDereferencer(
|
||||
state *state.State,
|
||||
converter *typeutils.Converter,
|
||||
transportController transport.Controller,
|
||||
visFilter *visibility.Filter,
|
||||
intFilter *interaction.Filter,
|
||||
mediaManager *media.Manager,
|
||||
) Dereferencer {
|
||||
return Dereferencer{
|
||||
|
|
@ -115,7 +119,8 @@ func NewDereferencer(
|
|||
converter: converter,
|
||||
transportController: transportController,
|
||||
mediaManager: mediaManager,
|
||||
visibility: visFilter,
|
||||
visFilter: visFilter,
|
||||
intFilter: intFilter,
|
||||
derefMedia: make(map[string]*media.ProcessingMedia),
|
||||
derefEmojis: make(map[string]*media.ProcessingEmoji),
|
||||
handshakes: make(map[string][]*url.URL),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
|
|
@ -79,8 +80,19 @@ func (suite *DereferencerStandardTestSuite) SetupTest() {
|
|||
suite.state.Storage = suite.storage
|
||||
|
||||
visFilter := visibility.NewFilter(&suite.state)
|
||||
intFilter := interaction.NewFilter(&suite.state)
|
||||
media := testrig.NewTestMediaManager(&suite.state)
|
||||
suite.dereferencer = dereferencing.NewDereferencer(&suite.state, converter, testrig.NewTestTransportController(&suite.state, suite.client), visFilter, media)
|
||||
suite.dereferencer = dereferencing.NewDereferencer(
|
||||
&suite.state,
|
||||
converter,
|
||||
testrig.NewTestTransportController(
|
||||
&suite.state,
|
||||
suite.client,
|
||||
),
|
||||
visFilter,
|
||||
intFilter,
|
||||
media,
|
||||
)
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -502,7 +502,8 @@ func (d *Dereferencer) enrichStatus(
|
|||
latestStatus.Local = status.Local
|
||||
|
||||
// Check if this is a permitted status we should accept.
|
||||
permit, err := d.isPermittedStatus(ctx, status, latestStatus)
|
||||
// Function also sets "PendingApproval" bool as necessary.
|
||||
permit, err := d.isPermittedStatus(ctx, requestUser, status, latestStatus)
|
||||
if err != nil {
|
||||
return nil, nil, gtserror.Newf("error checking permissibility for status %s: %w", uri, err)
|
||||
}
|
||||
|
|
@ -560,86 +561,6 @@ func (d *Dereferencer) enrichStatus(
|
|||
return latestStatus, apubStatus, nil
|
||||
}
|
||||
|
||||
// isPermittedStatus returns whether the given status
|
||||
// is permitted to be stored on this instance, checking
|
||||
// whether the author is suspended, and passes visibility
|
||||
// checks against status being replied-to (if any).
|
||||
func (d *Dereferencer) isPermittedStatus(
|
||||
ctx context.Context,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
permitted bool, // is permitted?
|
||||
err error,
|
||||
) {
|
||||
|
||||
// our failure condition handling
|
||||
// at the end of this function for
|
||||
// the case of permission = false.
|
||||
onFail := func() (bool, error) {
|
||||
if existing != nil {
|
||||
log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
|
||||
|
||||
// Delete existing status from database as it's no longer permitted.
|
||||
if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil {
|
||||
log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !status.Account.SuspendedAt.IsZero() {
|
||||
// The status author is suspended,
|
||||
// this shouldn't have reached here
|
||||
// but it's a fast check anyways.
|
||||
return onFail()
|
||||
}
|
||||
|
||||
if status.InReplyToURI == "" {
|
||||
// This status isn't in
|
||||
// reply to anything!
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if status.InReplyTo == nil {
|
||||
// If no inReplyTo has been set,
|
||||
// we return here for now as we
|
||||
// can't perform further checks.
|
||||
//
|
||||
// Worst case we allow something
|
||||
// through, and later on during
|
||||
// refetch it will get deleted.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if status.InReplyTo.BoostOfID != "" {
|
||||
// We do not permit replies to
|
||||
// boost wrapper statuses. (this
|
||||
// shouldn't be able to happen).
|
||||
return onFail()
|
||||
}
|
||||
|
||||
// Default to true
|
||||
permitted = true
|
||||
|
||||
if *status.InReplyTo.Local {
|
||||
// Check visibility of inReplyTo to status author.
|
||||
permitted, err = d.visibility.StatusVisible(ctx,
|
||||
status.Account,
|
||||
status.InReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking in-reply-to visibility: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if permitted {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return onFail()
|
||||
}
|
||||
|
||||
func (d *Dereferencer) fetchStatusMentions(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
|
|
|
|||
216
internal/federation/dereferencing/status_permitted.go
Normal file
216
internal/federation/dereferencing/status_permitted.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// 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 dereferencing
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
)
|
||||
|
||||
// isPermittedStatus returns whether the given status
|
||||
// is permitted to be stored on this instance, checking:
|
||||
//
|
||||
// - author is not suspended
|
||||
// - status passes visibility checks
|
||||
// - status passes interaction policy checks
|
||||
//
|
||||
// If status is not permitted to be stored, the function
|
||||
// will clean up after itself by removing the status.
|
||||
//
|
||||
// If status is a reply or a boost, and the author of
|
||||
// the given status is only permitted to reply or boost
|
||||
// pending approval, then "PendingApproval" will be set
|
||||
// to "true" on status. Callers should check this
|
||||
// and handle it as appropriate.
|
||||
func (d *Dereferencer) isPermittedStatus(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
existing *gtsmodel.Status,
|
||||
status *gtsmodel.Status,
|
||||
) (
|
||||
bool, // is permitted?
|
||||
error,
|
||||
) {
|
||||
// our failure condition handling
|
||||
// at the end of this function for
|
||||
// the case of permission = false.
|
||||
onFalse := func() (bool, error) {
|
||||
if existing != nil {
|
||||
log.Infof(ctx, "deleting unpermitted: %s", existing.URI)
|
||||
|
||||
// Delete existing status from database as it's no longer permitted.
|
||||
if err := d.state.DB.DeleteStatusByID(ctx, existing.ID); err != nil {
|
||||
log.Errorf(ctx, "error deleting %s after permissivity fail: %v", existing.URI, err)
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if status.Account.IsSuspended() {
|
||||
// The status author is suspended,
|
||||
// this shouldn't have reached here
|
||||
// but it's a fast check anyways.
|
||||
log.Debugf(ctx,
|
||||
"status author %s is suspended",
|
||||
status.AccountURI,
|
||||
)
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
if inReplyTo := status.InReplyTo; inReplyTo != nil {
|
||||
return d.isPermittedReply(
|
||||
ctx,
|
||||
requestUser,
|
||||
status,
|
||||
inReplyTo,
|
||||
onFalse,
|
||||
)
|
||||
} else if boostOf := status.BoostOf; boostOf != nil {
|
||||
return d.isPermittedBoost(
|
||||
ctx,
|
||||
requestUser,
|
||||
status,
|
||||
boostOf,
|
||||
onFalse,
|
||||
)
|
||||
}
|
||||
|
||||
// Nothing else stopping this.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) isPermittedReply(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
status *gtsmodel.Status,
|
||||
inReplyTo *gtsmodel.Status,
|
||||
onFalse func() (bool, error),
|
||||
) (bool, error) {
|
||||
if inReplyTo.BoostOfID != "" {
|
||||
// We do not permit replies to
|
||||
// boost wrapper statuses. (this
|
||||
// shouldn't be able to happen).
|
||||
log.Info(ctx, "rejecting reply to boost wrapper status")
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Check visibility of local
|
||||
// inReplyTo to replying account.
|
||||
if inReplyTo.IsLocal() {
|
||||
visible, err := d.visFilter.StatusVisible(ctx,
|
||||
status.Account,
|
||||
inReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking inReplyTo visibility: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Our status is not visible to the
|
||||
// account trying to do the reply.
|
||||
if !visible {
|
||||
return onFalse()
|
||||
}
|
||||
}
|
||||
|
||||
// Check interaction policy of inReplyTo.
|
||||
replyable, err := d.intFilter.StatusReplyable(ctx,
|
||||
status.Account,
|
||||
inReplyTo,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking status replyability: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if replyable.Forbidden() {
|
||||
// Replier is not permitted
|
||||
// to do this interaction.
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// TODO in next PR: check conditional /
|
||||
// with approval and deref Accept.
|
||||
if !replyable.Permitted() {
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *Dereferencer) isPermittedBoost(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
status *gtsmodel.Status,
|
||||
boostOf *gtsmodel.Status,
|
||||
onFalse func() (bool, error),
|
||||
) (bool, error) {
|
||||
if boostOf.BoostOfID != "" {
|
||||
// We do not permit boosts of
|
||||
// boost wrapper statuses. (this
|
||||
// shouldn't be able to happen).
|
||||
log.Info(ctx, "rejecting boost of boost wrapper status")
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Check visibility of local
|
||||
// boostOf to boosting account.
|
||||
if boostOf.IsLocal() {
|
||||
visible, err := d.visFilter.StatusVisible(ctx,
|
||||
status.Account,
|
||||
boostOf,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking boostOf visibility: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Our status is not visible to the
|
||||
// account trying to do the boost.
|
||||
if !visible {
|
||||
return onFalse()
|
||||
}
|
||||
}
|
||||
|
||||
// Check interaction policy of boostOf.
|
||||
boostable, err := d.intFilter.StatusBoostable(ctx,
|
||||
status.Account,
|
||||
boostOf,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking status boostability: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if boostable.Forbidden() {
|
||||
// Booster is not permitted
|
||||
// to do this interaction.
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// TODO in next PR: check conditional /
|
||||
// with approval and deref Accept.
|
||||
if !boostable.Permitted() {
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue