Browse Source
* [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>
pull/3142/head
61 changed files with 1682 additions and 606 deletions
@ -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 |
||||
} |
||||
@ -0,0 +1,34 @@
|
||||
// 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 interaction |
||||
|
||||
import ( |
||||
"github.com/superseriousbusiness/gotosocial/internal/state" |
||||
) |
||||
|
||||
// Filter packages up logic for checking whether
|
||||
// an interaction is permitted within set policies.
|
||||
type Filter struct { |
||||
state *state.State |
||||
} |
||||
|
||||
// NewFilter returns a new Filter
|
||||
// that will use the provided state.
|
||||
func NewFilter(state *state.State) *Filter { |
||||
return &Filter{state: state} |
||||
} |
||||
@ -0,0 +1,561 @@
|
||||
// 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 interaction |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"slices" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/superseriousbusiness/gotosocial/internal/log" |
||||
"github.com/superseriousbusiness/gotosocial/internal/util" |
||||
) |
||||
|
||||
type matchType int |
||||
|
||||
const ( |
||||
none matchType = 0 |
||||
implicit matchType = 1 |
||||
explicit matchType = 2 |
||||
) |
||||
|
||||
// startedThread returns true if requester started
|
||||
// the thread that the given status is part of.
|
||||
// Ie., requester created the first post in the thread.
|
||||
func (f *Filter) startedThread( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) (bool, error) { |
||||
parents, err := f.state.DB.GetStatusParents(ctx, status) |
||||
if err != nil { |
||||
return false, fmt.Errorf("db error getting parents of %s: %w", status.ID, err) |
||||
} |
||||
|
||||
if len(parents) == 0 { |
||||
// No parents available. Just check
|
||||
// if this status belongs to requester.
|
||||
return status.AccountID == requester.ID, nil |
||||
} |
||||
|
||||
// Check if OG status owned by requester.
|
||||
return parents[0].AccountID == requester.ID, nil |
||||
} |
||||
|
||||
// StatusLikeable checks if the given status
|
||||
// is likeable by the requester account.
|
||||
//
|
||||
// Callers to this function should have already
|
||||
// checked the visibility of status to requester,
|
||||
// including taking account of blocks, as this
|
||||
// function does not do visibility checks, only
|
||||
// interaction policy checks.
|
||||
func (f *Filter) StatusLikeable( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) (*gtsmodel.PolicyCheckResult, error) { |
||||
if requester.ID == status.AccountID { |
||||
// Status author themself can
|
||||
// always like their own status,
|
||||
// no need for further checks.
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), |
||||
}, nil |
||||
} |
||||
|
||||
switch { |
||||
// If status has policy set, check against that.
|
||||
case status.InteractionPolicy != nil: |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
status.InteractionPolicy.CanLike, |
||||
) |
||||
|
||||
// If status is local and has no policy set,
|
||||
// check against the default policy for this
|
||||
// visibility, as we're interaction-policy aware.
|
||||
case *status.Local: |
||||
policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
policy.CanLike, |
||||
) |
||||
|
||||
// Otherwise, assume the status is from an
|
||||
// instance that does not use / does not care
|
||||
// about interaction policies, and just return OK.
|
||||
default: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
// StatusReplyable checks if the given status
|
||||
// is replyable by the requester account.
|
||||
//
|
||||
// Callers to this function should have already
|
||||
// checked the visibility of status to requester,
|
||||
// including taking account of blocks, as this
|
||||
// function does not do visibility checks, only
|
||||
// interaction policy checks.
|
||||
func (f *Filter) StatusReplyable( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) (*gtsmodel.PolicyCheckResult, error) { |
||||
if util.PtrOrValue(status.PendingApproval, false) { |
||||
// Target status is pending approval,
|
||||
// check who started this thread.
|
||||
startedThread, err := f.startedThread( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
) |
||||
if err != nil { |
||||
err := gtserror.Newf("error checking thread ownership: %w", err) |
||||
return nil, err |
||||
} |
||||
|
||||
if !startedThread { |
||||
// If status is itself still pending approval,
|
||||
// and the requester didn't start this thread,
|
||||
// then buddy, any status that tries to reply
|
||||
// to it must be pending approval too. We do
|
||||
// this to prevent someone replying to a status
|
||||
// with a policy set that causes that reply to
|
||||
// require approval, *THEN* replying to their
|
||||
// own reply (which may not have a policy set)
|
||||
// and having the reply-to-their-own-reply go
|
||||
// through as Permitted. None of that!
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionWithApproval, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
if requester.ID == status.AccountID { |
||||
// Status author themself can
|
||||
// always reply to their own status,
|
||||
// no need for further checks.
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), |
||||
}, nil |
||||
} |
||||
|
||||
// If requester is replied to by this status,
|
||||
// then just return OK, it's functionally equivalent
|
||||
// to them being mentioned, and easier to check!
|
||||
if status.InReplyToAccountID == requester.ID { |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueMentioned), |
||||
}, nil |
||||
} |
||||
|
||||
// Check if requester mentioned by this status.
|
||||
//
|
||||
// Prefer checking by ID, fall back to URI, URL,
|
||||
// or NameString for not-yet enriched statuses.
|
||||
mentioned := slices.ContainsFunc( |
||||
status.Mentions, |
||||
func(m *gtsmodel.Mention) bool { |
||||
switch { |
||||
|
||||
// Check by ID - most accurate.
|
||||
case m.TargetAccountID != "": |
||||
return m.TargetAccountID == requester.ID |
||||
|
||||
// Check by URI - also accurate.
|
||||
case m.TargetAccountURI != "": |
||||
return m.TargetAccountURI == requester.URI |
||||
|
||||
// Check by URL - probably accurate.
|
||||
case m.TargetAccountURL != "": |
||||
return m.TargetAccountURL == requester.URL |
||||
|
||||
// Fall back to checking by namestring.
|
||||
case m.NameString != "": |
||||
username, host, err := util.ExtractNamestringParts(m.NameString) |
||||
if err != nil { |
||||
log.Debugf(ctx, "error checking if mentioned: %v", err) |
||||
return false |
||||
} |
||||
|
||||
if requester.IsLocal() { |
||||
// Local requester has empty string
|
||||
// domain so check using config.
|
||||
return username == requester.Username && |
||||
(host == config.GetHost() || host == config.GetAccountDomain()) |
||||
} |
||||
|
||||
// Remote requester has domain set.
|
||||
return username == requester.Username && |
||||
host == requester.Domain |
||||
|
||||
default: |
||||
// Not mentioned.
|
||||
return false |
||||
} |
||||
}, |
||||
) |
||||
|
||||
if mentioned { |
||||
// A mentioned account can always
|
||||
// reply, no need for further checks.
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueMentioned), |
||||
}, nil |
||||
} |
||||
|
||||
switch { |
||||
// If status has policy set, check against that.
|
||||
case status.InteractionPolicy != nil: |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
status.InteractionPolicy.CanReply, |
||||
) |
||||
|
||||
// If status is local and has no policy set,
|
||||
// check against the default policy for this
|
||||
// visibility, as we're interaction-policy aware.
|
||||
case *status.Local: |
||||
policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
policy.CanReply, |
||||
) |
||||
|
||||
// Otherwise, assume the status is from an
|
||||
// instance that does not use / does not care
|
||||
// about interaction policies, and just return OK.
|
||||
default: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
// StatusBoostable checks if the given status
|
||||
// is boostable by the requester account.
|
||||
//
|
||||
// Callers to this function should have already
|
||||
// checked the visibility of status to requester,
|
||||
// including taking account of blocks, as this
|
||||
// function does not do visibility checks, only
|
||||
// interaction policy checks.
|
||||
func (f *Filter) StatusBoostable( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) (*gtsmodel.PolicyCheckResult, error) { |
||||
if status.Visibility == gtsmodel.VisibilityDirect { |
||||
log.Trace(ctx, "direct statuses are not boostable") |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionForbidden, |
||||
}, nil |
||||
} |
||||
|
||||
if requester.ID == status.AccountID { |
||||
// Status author themself can
|
||||
// always boost non-directs,
|
||||
// no need for further checks.
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: util.Ptr(gtsmodel.PolicyValueAuthor), |
||||
}, nil |
||||
} |
||||
|
||||
switch { |
||||
// If status has policy set, check against that.
|
||||
case status.InteractionPolicy != nil: |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
status.InteractionPolicy.CanAnnounce, |
||||
) |
||||
|
||||
// If status is local and has no policy set,
|
||||
// check against the default policy for this
|
||||
// visibility, as we're interaction-policy aware.
|
||||
case *status.Local: |
||||
policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) |
||||
return f.checkPolicy( |
||||
ctx, |
||||
requester, |
||||
status, |
||||
policy.CanAnnounce, |
||||
) |
||||
|
||||
// Otherwise, assume the status is from an
|
||||
// instance that does not use / does not care
|
||||
// about interaction policies, and just return OK.
|
||||
default: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
}, nil |
||||
} |
||||
} |
||||
|
||||
func (f *Filter) checkPolicy( |
||||
ctx context.Context, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
rules gtsmodel.PolicyRules, |
||||
) (*gtsmodel.PolicyCheckResult, error) { |
||||
|
||||
// Wrap context to be able to
|
||||
// cache some database calls.
|
||||
fctx := new(filterctx) |
||||
fctx.Context = ctx |
||||
|
||||
// Check if requester matches a PolicyValue
|
||||
// to be always allowed to do this.
|
||||
matchAlways, matchAlwaysValue, err := f.matchPolicy(fctx, |
||||
requester, |
||||
status, |
||||
rules.Always, |
||||
) |
||||
if err != nil { |
||||
return nil, gtserror.Newf("error checking policy match: %w", err) |
||||
} |
||||
|
||||
// Check if requester matches a PolicyValue
|
||||
// to be allowed to do this pending approval.
|
||||
matchWithApproval, _, err := f.matchPolicy(fctx, |
||||
requester, |
||||
status, |
||||
rules.WithApproval, |
||||
) |
||||
if err != nil { |
||||
return nil, gtserror.Newf("error checking policy approval match: %w", err) |
||||
} |
||||
|
||||
switch { |
||||
|
||||
// Prefer explicit match,
|
||||
// prioritizing "always".
|
||||
case matchAlways == explicit: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: &matchAlwaysValue, |
||||
}, nil |
||||
|
||||
case matchWithApproval == explicit: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionWithApproval, |
||||
}, nil |
||||
|
||||
// Then try implicit match,
|
||||
// prioritizing "always".
|
||||
case matchAlways == implicit: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionPermitted, |
||||
PermittedMatchedOn: &matchAlwaysValue, |
||||
}, nil |
||||
|
||||
case matchWithApproval == implicit: |
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionWithApproval, |
||||
}, nil |
||||
} |
||||
|
||||
// No match.
|
||||
return >smodel.PolicyCheckResult{ |
||||
Permission: gtsmodel.PolicyPermissionForbidden, |
||||
}, nil |
||||
} |
||||
|
||||
// matchPolicy returns whether requesting account
|
||||
// matches any of the policy values for given status,
|
||||
// returning the policy it matches on and match type.
|
||||
// uses a *filterctx to cache certain db results.
|
||||
func (f *Filter) matchPolicy( |
||||
ctx *filterctx, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
policyValues []gtsmodel.PolicyValue, |
||||
) ( |
||||
matchType, |
||||
gtsmodel.PolicyValue, |
||||
error, |
||||
) { |
||||
var ( |
||||
match = none |
||||
value gtsmodel.PolicyValue |
||||
) |
||||
|
||||
for _, p := range policyValues { |
||||
switch p { |
||||
|
||||
// Check if anyone
|
||||
// can do this.
|
||||
case gtsmodel.PolicyValuePublic: |
||||
match = implicit |
||||
value = gtsmodel.PolicyValuePublic |
||||
|
||||
// Check if follower
|
||||
// of status owner.
|
||||
case gtsmodel.PolicyValueFollowers: |
||||
inFollowers, err := f.inFollowers(ctx, |
||||
requester, |
||||
status, |
||||
) |
||||
if err != nil { |
||||
return 0, "", err |
||||
} |
||||
if inFollowers { |
||||
match = implicit |
||||
value = gtsmodel.PolicyValueFollowers |
||||
} |
||||
|
||||
// Check if followed
|
||||
// by status owner.
|
||||
case gtsmodel.PolicyValueFollowing: |
||||
inFollowing, err := f.inFollowing(ctx, |
||||
requester, |
||||
status, |
||||
) |
||||
if err != nil { |
||||
return 0, "", err |
||||
} |
||||
if inFollowing { |
||||
match = implicit |
||||
value = gtsmodel.PolicyValueFollowing |
||||
} |
||||
|
||||
// Check if replied-to by or
|
||||
// mentioned in the status.
|
||||
case gtsmodel.PolicyValueMentioned: |
||||
if (status.InReplyToAccountID == requester.ID) || |
||||
status.MentionsAccount(requester.ID) { |
||||
// Return early as we've
|
||||
// found an explicit match.
|
||||
match = explicit |
||||
value = gtsmodel.PolicyValueMentioned |
||||
return match, value, nil |
||||
} |
||||
|
||||
// Check if PolicyValue specifies
|
||||
// requester explicitly.
|
||||
default: |
||||
if string(p) == requester.URI { |
||||
// Return early as we've
|
||||
// found an explicit match.
|
||||
match = explicit |
||||
value = gtsmodel.PolicyValue(requester.URI) |
||||
return match, value, nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Return either "" or "implicit",
|
||||
// and the policy value matched
|
||||
// against (if set).
|
||||
return match, value, nil |
||||
} |
||||
|
||||
// inFollowers returns whether requesting account is following
|
||||
// status author, uses *filterctx type for db result caching.
|
||||
func (f *Filter) inFollowers( |
||||
ctx *filterctx, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) ( |
||||
bool, |
||||
error, |
||||
) { |
||||
if ctx.inFollowersOnce == 0 { |
||||
var err error |
||||
|
||||
// Load the 'inFollowers' result from database.
|
||||
ctx.inFollowers, err = f.state.DB.IsFollowing(ctx, |
||||
requester.ID, |
||||
status.AccountID, |
||||
) |
||||
if err != nil { |
||||
return false, gtserror.Newf("error checking follow status: %w", err) |
||||
} |
||||
|
||||
// Mark value as stored.
|
||||
ctx.inFollowersOnce = 1 |
||||
} |
||||
|
||||
// Return stored value.
|
||||
return ctx.inFollowers, nil |
||||
} |
||||
|
||||
// inFollowing returns whether status author is following
|
||||
// requesting account, uses *filterctx for db result caching.
|
||||
func (f *Filter) inFollowing( |
||||
ctx *filterctx, |
||||
requester *gtsmodel.Account, |
||||
status *gtsmodel.Status, |
||||
) ( |
||||
bool, |
||||
error, |
||||
) { |
||||
if ctx.inFollowingOnce == 0 { |
||||
var err error |
||||
|
||||
// Load the 'inFollowers' result from database.
|
||||
ctx.inFollowing, err = f.state.DB.IsFollowing(ctx, |
||||
status.AccountID, |
||||
requester.ID, |
||||
) |
||||
if err != nil { |
||||
return false, gtserror.Newf("error checking follow status: %w", err) |
||||
} |
||||
|
||||
// Mark value as stored.
|
||||
ctx.inFollowingOnce = 1 |
||||
} |
||||
|
||||
// Return stored value.
|
||||
return ctx.inFollowing, nil |
||||
} |
||||
|
||||
// filterctx wraps a context.Context to also
|
||||
// store loadable data relevant to a fillter
|
||||
// operation from the database, such that it
|
||||
// only needs to be loaded once IF required.
|
||||
type filterctx struct { |
||||
context.Context |
||||
|
||||
inFollowers bool |
||||
inFollowersOnce int32 |
||||
|
||||
inFollowing bool |
||||
inFollowingOnce int32 |
||||
} |
||||
@ -1,57 +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 visibility |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
||||
"github.com/superseriousbusiness/gotosocial/internal/log" |
||||
) |
||||
|
||||
// StatusBoostable checks if given status is boostable by requester, checking boolean status visibility to requester and ultimately the AP status visibility setting.
|
||||
func (f *Filter) StatusBoostable(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) { |
||||
if status.Visibility == gtsmodel.VisibilityDirect { |
||||
log.Trace(ctx, "direct statuses are not boostable") |
||||
return false, nil |
||||
} |
||||
|
||||
// Check whether status is visible to requesting account.
|
||||
visible, err := f.StatusVisible(ctx, requester, status) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
if !visible { |
||||
log.Trace(ctx, "status not visible to requesting account") |
||||
return false, nil |
||||
} |
||||
|
||||
if requester.ID == status.AccountID { |
||||
// Status author can always boost non-directs.
|
||||
return true, nil |
||||
} |
||||
|
||||
if status.Visibility == gtsmodel.VisibilityFollowersOnly || |
||||
status.Visibility == gtsmodel.VisibilityMutualsOnly { |
||||
log.Trace(ctx, "unauthored %s status not boostable", status.Visibility) |
||||
return false, nil |
||||
} |
||||
|
||||
return true, nil |
||||
} |
||||
@ -1,154 +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 visibility_test |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/suite" |
||||
) |
||||
|
||||
type StatusBoostableTestSuite struct { |
||||
FilterStandardTestSuite |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnPublicBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_1"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnUnlockedBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_2"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyNonInteractiveBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_3"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnMutualsOnlyBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_4"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnFollowersOnlyBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_5"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOwnDirectNotBoostable() { |
||||
testStatus := suite.testStatuses["local_account_2_status_6"] |
||||
testAccount := suite.testAccounts["local_account_2"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.False(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOtherPublicBoostable() { |
||||
testStatus := suite.testStatuses["local_account_2_status_1"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOtherUnlistedBoostable() { |
||||
testStatus := suite.testStatuses["local_account_1_status_2"] |
||||
testAccount := suite.testAccounts["local_account_2"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.True(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOtherFollowersOnlyNotBoostable() { |
||||
testStatus := suite.testStatuses["local_account_2_status_7"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.False(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestOtherDirectNotBoostable() { |
||||
testStatus := suite.testStatuses["local_account_2_status_6"] |
||||
testAccount := suite.testAccounts["local_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.False(boostable) |
||||
} |
||||
|
||||
func (suite *StatusBoostableTestSuite) TestRemoteFollowersOnlyNotVisible() { |
||||
testStatus := suite.testStatuses["local_account_1_status_5"] |
||||
testAccount := suite.testAccounts["remote_account_1"] |
||||
ctx := context.Background() |
||||
|
||||
boostable, err := suite.filter.StatusBoostable(ctx, testAccount, testStatus) |
||||
suite.NoError(err) |
||||
|
||||
suite.False(boostable) |
||||
} |
||||
|
||||
func TestStatusBoostableTestSuite(t *testing.T) { |
||||
suite.Run(t, new(StatusBoostableTestSuite)) |
||||
} |
||||
Loading…
Reference in new issue