mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02: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:
34
internal/filter/interaction/filter.go
Normal file
34
internal/filter/interaction/filter.go
Normal file
@@ -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}
|
||||
}
|
561
internal/filter/interaction/interactable.go
Normal file
561
internal/filter/interaction/interactable.go
Normal file
@@ -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))
|
||||
}
|
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// StatusesVisible calls StatusVisible for each status in the statuses slice, and returns a slice of only statuses which are visible to the requester.
|
||||
@@ -41,8 +42,15 @@ func (f *Filter) StatusesVisible(ctx context.Context, requester *gtsmodel.Accoun
|
||||
return filtered, errs.Combine()
|
||||
}
|
||||
|
||||
// StatusVisible will check if given status is visible to requester, accounting for requester with no auth (i.e is nil), suspensions, disabled local users, account blocks and status privacy.
|
||||
func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
||||
// StatusVisible will check if status is visible to requester,
|
||||
// accounting for requester with no auth (i.e is nil), suspensions,
|
||||
// disabled local users, pending approvals, account blocks,
|
||||
// and status visibility settings.
|
||||
func (f *Filter) StatusVisible(
|
||||
ctx context.Context,
|
||||
requester *gtsmodel.Account,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
const vtype = cache.VisibilityTypeStatus
|
||||
|
||||
// By default we assume no auth.
|
||||
@@ -75,8 +83,14 @@ func (f *Filter) StatusVisible(ctx context.Context, requester *gtsmodel.Account,
|
||||
return visibility.Value, nil
|
||||
}
|
||||
|
||||
// isStatusVisible will check if status is visible to requester. It is the "meat" of the logic to Filter{}.StatusVisible() which is called within cache loader callback.
|
||||
func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
||||
// isStatusVisible will check if status is visible to requester.
|
||||
// It is the "meat" of the logic to Filter{}.StatusVisible()
|
||||
// which is called within cache loader callback.
|
||||
func (f *Filter) isStatusVisible(
|
||||
ctx context.Context,
|
||||
requester *gtsmodel.Account,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
// Ensure that status is fully populated for further processing.
|
||||
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return false, gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
@@ -90,6 +104,14 @@ func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Accoun
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if util.PtrOrValue(status.PendingApproval, false) {
|
||||
// Use a different visibility heuristic
|
||||
// for pending approval statuses.
|
||||
return f.isPendingStatusVisible(ctx,
|
||||
requester, status,
|
||||
)
|
||||
}
|
||||
|
||||
if status.Visibility == gtsmodel.VisibilityPublic {
|
||||
// This status will be visible to all.
|
||||
return true, nil
|
||||
@@ -176,6 +198,41 @@ func (f *Filter) isStatusVisible(ctx context.Context, requester *gtsmodel.Accoun
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) isPendingStatusVisible(
|
||||
_ context.Context,
|
||||
requester *gtsmodel.Account,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
if requester == nil {
|
||||
// Any old tom, dick, and harry can't
|
||||
// see pending-approval statuses,
|
||||
// no matter what their visibility.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if status.AccountID == requester.ID {
|
||||
// This is requester's status,
|
||||
// so they can always see it.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if status.InReplyToAccountID == requester.ID {
|
||||
// This status replies to requester,
|
||||
// so they can always see it (else
|
||||
// they can't approve it).
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if status.BoostOfAccountID == requester.ID {
|
||||
// This status boosts requester,
|
||||
// so they can always see it.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Nobody else can see this.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// areStatusAccountsVisible calls Filter{}.AccountVisible() on status author and the status boost-of (if set) author, returning visibility of status (and boost-of) to requester.
|
||||
func (f *Filter) areStatusAccountsVisible(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status) (bool, error) {
|
||||
// Check whether status author's account is visible to requester.
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
type StatusVisibleTestSuite struct {
|
||||
@@ -156,6 +157,49 @@ func (suite *StatusVisibleTestSuite) TestStatusNotVisibleIfNotFollowingCached()
|
||||
suite.False(visible)
|
||||
}
|
||||
|
||||
func (suite *StatusVisibleTestSuite) TestVisiblePending() {
|
||||
ctx := context.Background()
|
||||
|
||||
// Copy the test status and mark
|
||||
// the copy as pending approval.
|
||||
//
|
||||
// This is a status from admin
|
||||
// that replies to zork.
|
||||
testStatus := new(gtsmodel.Status)
|
||||
*testStatus = *suite.testStatuses["admin_account_status_3"]
|
||||
testStatus.PendingApproval = util.Ptr(true)
|
||||
|
||||
for _, testCase := range []struct {
|
||||
acct *gtsmodel.Account
|
||||
visible bool
|
||||
}{
|
||||
{
|
||||
acct: suite.testAccounts["admin_account"],
|
||||
visible: true, // Own status, always visible.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["local_account_1"],
|
||||
visible: true, // Reply to zork, always visible.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["local_account_2"],
|
||||
visible: false, // None of their business.
|
||||
},
|
||||
{
|
||||
acct: suite.testAccounts["remote_account_1"],
|
||||
visible: false, // None of their business.
|
||||
},
|
||||
{
|
||||
acct: nil, // Unauthed request.
|
||||
visible: false, // None of their business.
|
||||
},
|
||||
} {
|
||||
visible, err := suite.filter.StatusVisible(ctx, testCase.acct, testStatus)
|
||||
suite.NoError(err)
|
||||
suite.Equal(testCase.visible, visible)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusVisibleTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusVisibleTestSuite))
|
||||
}
|
||||
|
Reference in New Issue
Block a user