From 56f98dc3b98d7220947957551ec5aa2f82f9b997 Mon Sep 17 00:00:00 2001 From: tobi Date: Tue, 10 Jun 2025 14:29:42 +0200 Subject: [PATCH] [chore] Update interactionPolicy sub-policy parsing in line with documented defaults (#4229) > If this is a code change, please include a summary of what you've coded, and link to the issue(s) it closes/implements. > > If this is a documentation change, please briefly describe what you've changed and why. Brings our parsing of unset sub-policies in line with the defaults documented here: https://docs.gotosocial.org/en/v0.19.1/federation/interaction_policy/#defaults-per-sub-policy Closes https://codeberg.org/superseriousbusiness/gotosocial/issues/4146 Part of https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 Please put an x inside each checkbox to indicate that you've read and followed it: `[ ]` -> `[x]` If this is a documentation change, only the first checkbox must be filled (you can delete the others if you want). - [x] I/we have read the [GoToSocial contribution guidelines](https://codeberg.org/superseriousbusiness/gotosocial/src/branch/main/CONTRIBUTING.md). - [x] I/we have discussed the proposed changes already, either in an issue on the repository, or in the Matrix chat. - [x] I/we have not leveraged AI to create the proposed changes. - [x] I/we have performed a self-review of added code. - [x] I/we have written code that is legible and maintainable by others. - [x] I/we have commented the added code, particularly in hard-to-understand areas. - [ ] I/we have made any necessary changes to documentation. - [x] I/we have added tests that cover new code. - [x] I/we have run tests and they pass locally with the changes. - [x] I/we have run `go fmt ./...` and `golangci-lint run`. Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4229 Co-authored-by: tobi Co-committed-by: tobi --- internal/ap/extract.go | 44 ++-- internal/ap/extractpolicy_test.go | 6 +- internal/filter/interaction/interactable.go | 58 ++-- .../filter/interaction/interactable_test.go | 207 +++++++++++++++ internal/gtsmodel/interactionpolicy.go | 248 ++++++++++++------ internal/typeutils/frontendtointernal.go | 6 +- internal/typeutils/internaltoas.go | 13 + internal/typeutils/internaltofrontend.go | 54 +++- testrig/testmodels.go | 24 +- testrig/teststructs.go | 2 + 10 files changed, 507 insertions(+), 155 deletions(-) create mode 100644 internal/filter/interaction/interactable_test.go diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 2387c1f9a..776471538 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -1074,6 +1074,9 @@ func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmo // Will be nil (default policy) for Statusables that have no policy // set on them, or have a null policy. In such a case, the caller // should assume the default policy for the status's visibility level. +// +// Sub-policies of the returned policy, eg., CanLike, CanReply, may +// each be nil if they were not set on the interaction policy. func ExtractInteractionPolicy( statusable Statusable, owner *gtsmodel.Account, @@ -1100,6 +1103,8 @@ func ExtractInteractionPolicy( return nil } + // There's a policy key/value + // set, extract sub-policies. return >smodel.InteractionPolicy{ CanLike: extractCanLike(policy.GetGoToSocialCanLike(), owner), CanReply: extractCanReply(policy.GetGoToSocialCanReply(), owner), @@ -1107,73 +1112,82 @@ func ExtractInteractionPolicy( } } +// Returns either a parsed CanLike sub-policy, or nil +// if canLike is not set, ie., if this post is from an +// instance that doesn't know / care about canLike. func extractCanLike( prop vocab.GoToSocialCanLikeProperty, owner *gtsmodel.Account, -) gtsmodel.PolicyRules { +) *gtsmodel.PolicyRules { if prop == nil || prop.Len() != 1 { - return gtsmodel.PolicyRules{} + return nil } propIter := prop.At(0) if !propIter.IsGoToSocialCanLike() { - return gtsmodel.PolicyRules{} + return nil } withRules := propIter.Get() if withRules == nil { - return gtsmodel.PolicyRules{} + return nil } - return gtsmodel.PolicyRules{ + return >smodel.PolicyRules{ Always: extractPolicyValues(withRules.GetGoToSocialAlways(), owner), WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), owner), } } +// Returns either a parsed CanReply sub-policy, or nil +// if canReply is not set, ie., if this post is from an +// instance that doesn't know / care about canReply. func extractCanReply( prop vocab.GoToSocialCanReplyProperty, owner *gtsmodel.Account, -) gtsmodel.PolicyRules { +) *gtsmodel.PolicyRules { if prop == nil || prop.Len() != 1 { - return gtsmodel.PolicyRules{} + return nil } propIter := prop.At(0) if !propIter.IsGoToSocialCanReply() { - return gtsmodel.PolicyRules{} + return nil } withRules := propIter.Get() if withRules == nil { - return gtsmodel.PolicyRules{} + return nil } - return gtsmodel.PolicyRules{ + return >smodel.PolicyRules{ Always: extractPolicyValues(withRules.GetGoToSocialAlways(), owner), WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), owner), } } +// Returns either a parsed CanAnnounce sub-policy, or nil +// if canAnnounce is not set, ie., if this post is from an +// instance that doesn't know / care about canAnnounce. func extractCanAnnounce( prop vocab.GoToSocialCanAnnounceProperty, owner *gtsmodel.Account, -) gtsmodel.PolicyRules { +) *gtsmodel.PolicyRules { if prop == nil || prop.Len() != 1 { - return gtsmodel.PolicyRules{} + return nil } propIter := prop.At(0) if !propIter.IsGoToSocialCanAnnounce() { - return gtsmodel.PolicyRules{} + return nil } withRules := propIter.Get() if withRules == nil { - return gtsmodel.PolicyRules{} + return nil } - return gtsmodel.PolicyRules{ + return >smodel.PolicyRules{ Always: extractPolicyValues(withRules.GetGoToSocialAlways(), owner), WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), owner), } diff --git a/internal/ap/extractpolicy_test.go b/internal/ap/extractpolicy_test.go index a5e8db6a7..f939d8bca 100644 --- a/internal/ap/extractpolicy_test.go +++ b/internal/ap/extractpolicy_test.go @@ -103,13 +103,13 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicy() { ) expectedPolicy := >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{ gtsmodel.PolicyValuePublic, }, WithApproval: gtsmodel.PolicyValues{}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, gtsmodel.PolicyValueFollowers, @@ -120,7 +120,7 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicy() { gtsmodel.PolicyValuePublic, }, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, }, diff --git a/internal/filter/interaction/interactable.go b/internal/filter/interaction/interactable.go index e8afbd83c..f761edb55 100644 --- a/internal/filter/interaction/interactable.go +++ b/internal/filter/interaction/interactable.go @@ -84,8 +84,8 @@ func (f *Filter) StatusLikeable( } switch { - // If status has policy set, check against that. - case status.InteractionPolicy != nil: + // If status has canLike sub-policy set, check against that. + case status.InteractionPolicy != nil && status.InteractionPolicy.CanLike != nil: return f.checkPolicy( ctx, requester, @@ -95,19 +95,18 @@ func (f *Filter) StatusLikeable( // If status is local and has no policy set, // check against the default policy for this - // visibility, as we're interaction-policy aware. + // visibility, as we're canLike sub-policy aware. case *status.Local: - policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) return f.checkPolicy( ctx, requester, status, - policy.CanLike, + gtsmodel.DefaultCanLikeFor(status.Visibility), ) // Otherwise, assume the status is from an // instance that does not use / does not care - // about interaction policies, and just return OK. + // about canLike sub-policy, and just return OK. default: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionPermitted, @@ -235,8 +234,8 @@ func (f *Filter) StatusReplyable( } switch { - // If status has policy set, check against that. - case status.InteractionPolicy != nil: + // If status has canReply sub-policy set, check against that. + case status.InteractionPolicy != nil && status.InteractionPolicy.CanReply != nil: return f.checkPolicy( ctx, requester, @@ -245,20 +244,19 @@ func (f *Filter) StatusReplyable( ) // If status is local and has no policy set, - // check against the default policy for this - // visibility, as we're interaction-policy aware. + // check against the default canReply for this + // visibility, as we're canReply sub-policy aware. case *status.Local: - policy := gtsmodel.DefaultInteractionPolicyFor(status.Visibility) return f.checkPolicy( ctx, requester, status, - policy.CanReply, + gtsmodel.DefaultCanReplyFor(status.Visibility), ) // Otherwise, assume the status is from an // instance that does not use / does not care - // about interaction policies, and just return OK. + // about canReply sub-policy, and just return OK. default: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionPermitted, @@ -297,8 +295,8 @@ func (f *Filter) StatusBoostable( } switch { - // If status has policy set, check against that. - case status.InteractionPolicy != nil: + // If status has canAnnounce sub-policy set, check against that. + case status.InteractionPolicy != nil && status.InteractionPolicy.CanAnnounce != nil: return f.checkPolicy( ctx, requester, @@ -319,7 +317,7 @@ func (f *Filter) StatusBoostable( ) // Status is from an instance that does not use - // or does not care about interaction policies. + // or does not care about canAnnounce sub-policy. // We can boost it if it's unlisted or public. case status.Visibility == gtsmodel.VisibilityPublic || status.Visibility == gtsmodel.VisibilityUnlocked: @@ -340,7 +338,7 @@ func (f *Filter) checkPolicy( ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, - rules gtsmodel.PolicyRules, + rules *gtsmodel.PolicyRules, ) (*gtsmodel.PolicyCheckResult, error) { // Wrap context to be able to @@ -349,8 +347,8 @@ func (f *Filter) checkPolicy( fctx.Context = ctx // Check if requester matches a PolicyValue - // to be always allowed to do this. - matchAlways, matchAlwaysValue, err := f.matchPolicy(fctx, + // to be automatically approved for this. + matchAutomatic, matchAutomaticValue, err := f.matchPolicy(fctx, requester, status, rules.Always, @@ -360,40 +358,40 @@ func (f *Filter) checkPolicy( } // Check if requester matches a PolicyValue - // to be allowed to do this pending approval. - matchWithApproval, _, err := f.matchPolicy(fctx, + // to be manually approved for this. + matchManual, _, err := f.matchPolicy(fctx, requester, status, rules.WithApproval, ) if err != nil { - return nil, gtserror.Newf("error checking policy approval match: %w", err) + return nil, gtserror.Newf("error checking policy match: %w", err) } switch { // Prefer explicit match, - // prioritizing "always". - case matchAlways == explicit: + // prioritizing automatic. + case matchAutomatic == explicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionPermitted, - PermittedMatchedOn: &matchAlwaysValue, + PermittedMatchedOn: &matchAutomaticValue, }, nil - case matchWithApproval == explicit: + case matchManual == explicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionWithApproval, }, nil // Then try implicit match, - // prioritizing "always". - case matchAlways == implicit: + // prioritizing automatic. + case matchAutomatic == implicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionPermitted, - PermittedMatchedOn: &matchAlwaysValue, + PermittedMatchedOn: &matchAutomaticValue, }, nil - case matchWithApproval == implicit: + case matchManual == implicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionWithApproval, }, nil diff --git a/internal/filter/interaction/interactable_test.go b/internal/filter/interaction/interactable_test.go new file mode 100644 index 000000000..9eede31c0 --- /dev/null +++ b/internal/filter/interaction/interactable_test.go @@ -0,0 +1,207 @@ +// 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 . + +package interaction_test + +import ( + "strconv" + "testing" + + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/testrig" + "github.com/stretchr/testify/suite" +) + +const ( + rMediaPath = "../../../testrig/media" + rTemplatePath = "../../../web/template" +) + +type InteractionTestSuite struct { + suite.Suite + + testStatuses map[string]*gtsmodel.Status + testAccounts map[string]*gtsmodel.Account +} + +func (suite *InteractionTestSuite) SetupSuite() { + testrig.InitTestConfig() + testrig.InitTestLog() + + suite.testStatuses = testrig.NewTestStatuses() + suite.testAccounts = testrig.NewTestAccounts() +} + +func (suite *InteractionTestSuite) TestInteractable() { + testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath) + defer testrig.TearDownTestStructs(testStructs) + + // Take zork's introduction post + // as the base post for these tests. + modelStatus := suite.testStatuses["local_account_1_status_1"] + + ctx := suite.T().Context() + for i, test := range []struct { + policy *gtsmodel.InteractionPolicy + account *gtsmodel.Account + likeable gtsmodel.PolicyPermission + replyable gtsmodel.PolicyPermission + boostable gtsmodel.PolicyPermission + }{ + { + // Nil policy. Should all be fine as + // it will fall back to the default then. + policy: nil, + account: suite.testAccounts["admin_account"], + likeable: gtsmodel.PolicyPermissionAutomaticApproval, + replyable: gtsmodel.PolicyPermissionAutomaticApproval, + boostable: gtsmodel.PolicyPermissionAutomaticApproval, + }, + { + // Nil canLike, everything else + // restricted to author only. + // Only the nil sub-policy should be OK. + policy: >smodel.InteractionPolicy{ + CanLike: nil, + CanReply: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + }, + CanAnnounce: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + }, + }, + account: suite.testAccounts["admin_account"], + likeable: gtsmodel.PolicyPermissionAutomaticApproval, + replyable: gtsmodel.PolicyPermissionForbidden, + boostable: gtsmodel.PolicyPermissionForbidden, + }, + { + // All restricted it's the author's own + // account checking, so all should be fine. + policy: >smodel.InteractionPolicy{ + CanLike: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + }, + CanReply: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + }, + CanAnnounce: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + }, + }, + account: suite.testAccounts["local_account_1"], + likeable: gtsmodel.PolicyPermissionAutomaticApproval, + replyable: gtsmodel.PolicyPermissionAutomaticApproval, + boostable: gtsmodel.PolicyPermissionAutomaticApproval, + }, + { + // Followers can like automatically, + // everything else requires manual approval. + policy: >smodel.InteractionPolicy{ + CanLike: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + gtsmodel.PolicyValueFollowers, + }, + ManualApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValuePublic, + }, + }, + CanReply: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + ManualApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValuePublic, + }, + }, + CanAnnounce: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + ManualApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValuePublic, + }, + }, + }, + account: suite.testAccounts["admin_account"], + likeable: gtsmodel.PolicyPermissionAutomaticApproval, + replyable: gtsmodel.PolicyPermissionManualApproval, + boostable: gtsmodel.PolicyPermissionManualApproval, + }, + } { + // Copy model status. + status := new(gtsmodel.Status) + *status = *modelStatus + + // Set test policy on it. + status.InteractionPolicy = test.policy + + // Check likeableRes. + likeableRes, err := testStructs.InteractionFilter.StatusLikeable(ctx, test.account, status) + if err != nil { + suite.FailNow(err.Error()) + } + if likeableRes.Permission != test.likeable { + suite.Fail( + "failure in case "+strconv.FormatInt(int64(i), 10), + "expected likeable result \"%s\", got \"%s\"", + likeableRes.Permission, test.likeable, + ) + } + + // Check replable. + replyableRes, err := testStructs.InteractionFilter.StatusReplyable(ctx, test.account, status) + if err != nil { + suite.FailNow(err.Error()) + } + if replyableRes.Permission != test.replyable { + suite.Fail( + "failure in case "+strconv.FormatInt(int64(i), 10), + "expected replyable result \"%s\", got \"%s\"", + replyableRes.Permission, test.replyable, + ) + } + + // Check boostable. + boostableRes, err := testStructs.InteractionFilter.StatusBoostable(ctx, test.account, status) + if err != nil { + suite.FailNow(err.Error()) + } + if boostableRes.Permission != test.boostable { + suite.Fail( + "failure in case "+strconv.FormatInt(int64(i), 10), + "expected boostable result \"%s\", got \"%s\"", + boostableRes.Permission, test.boostable, + ) + } + } +} + +func TestInteractionTestSuite(t *testing.T) { + suite.Run(t, new(InteractionTestSuite)) +} diff --git a/internal/gtsmodel/interactionpolicy.go b/internal/gtsmodel/interactionpolicy.go index 7fcafc80d..59d91b03d 100644 --- a/internal/gtsmodel/interactionpolicy.go +++ b/internal/gtsmodel/interactionpolicy.go @@ -129,6 +129,19 @@ const ( PolicyPermissionPermitted ) +func (p PolicyPermission) String() string { + switch p { + case PolicyPermissionForbidden: + return "forbidden" + case PolicyPermissionWithApproval: + return "withApproval" + case PolicyPermissionPermitted: + return "always" + default: + return "unknown" + } +} + // PolicyCheckResult encapsulates the results // of checking a certain Actor URI + type // of interaction against an interaction policy. @@ -186,15 +199,15 @@ type InteractionPolicy struct { // Conditions in which a Like // interaction will be accepted // for an item with this policy. - CanLike PolicyRules + CanLike *PolicyRules // Conditions in which a Reply // interaction will be accepted // for an item with this policy. - CanReply PolicyRules + CanReply *PolicyRules // Conditions in which an Announce // interaction will be accepted // for an item with this policy. - CanAnnounce PolicyRules + CanAnnounce *PolicyRules } // PolicyRules represents the rules according @@ -228,37 +241,144 @@ func DefaultInteractionPolicyFor(v Visibility) *InteractionPolicy { } } -var defaultPolicyPublic = &InteractionPolicy{ - CanLike: PolicyRules{ - // Anyone can like. - Always: PolicyValues{ - PolicyValuePublic, - }, - WithApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Anyone can reply. - Always: PolicyValues{ - PolicyValuePublic, - }, - WithApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Anyone can announce. - Always: PolicyValues{ - PolicyValuePublic, - }, - WithApproval: make(PolicyValues, 0), - }, +// DefaultCanLikeFor returns the default +// policy rules for the canLike sub-policy. +func DefaultCanLikeFor(v Visibility) *PolicyRules { + switch v { + + // Anyone can like. + case VisibilityPublic, VisibilityUnlocked: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValuePublic, + }, + WithApproval: make(PolicyValues, 0), + } + + // Self, followers and + // mentioned can like. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValueAuthor, + PolicyValueFollowers, + PolicyValueMentioned, + }, + WithApproval: make(PolicyValues, 0), + } + + // Mentioned and self + // can always like. + case VisibilityDirect: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValueAuthor, + PolicyValueMentioned, + }, + WithApproval: make(PolicyValues, 0), + } + + default: + panic("invalid visibility") + } } -// Returns the default interaction policy +// DefaultCanReplyFor returns the default +// policy rules for the canReply sub-policy. +func DefaultCanReplyFor(v Visibility) *PolicyRules { + switch v { + + // Anyone can reply. + case VisibilityPublic, VisibilityUnlocked: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValuePublic, + }, + WithApproval: make(PolicyValues, 0), + } + + // Self, followers and + // mentioned can reply. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + + Always: PolicyValues{ + PolicyValueAuthor, + PolicyValueFollowers, + PolicyValueMentioned, + }, + WithApproval: make(PolicyValues, 0), + } + + // Mentioned and self + // can always reply. + case VisibilityDirect: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValueAuthor, + PolicyValueMentioned, + }, + WithApproval: make(PolicyValues, 0), + } + + default: + panic("invalid visibility") + } +} + +// DefaultCanAnnounceFor returns the default +// policy rules for the canAnnounce sub-policy. +func DefaultCanAnnounceFor(v Visibility) *PolicyRules { + switch v { + + // Anyone can announce. + case VisibilityPublic, VisibilityUnlocked: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValuePublic, + }, + WithApproval: make(PolicyValues, 0), + } + + // Only self can announce. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValueAuthor, + }, + WithApproval: make(PolicyValues, 0), + } + + // Only self can announce. + case VisibilityDirect: + return &PolicyRules{ + Always: PolicyValues{ + PolicyValueAuthor, + }, + WithApproval: make(PolicyValues, 0), + } + + default: + panic("invalid visibility") + } +} + +var defaultPolicyPublic = &InteractionPolicy{ + CanLike: DefaultCanLikeFor(VisibilityPublic), + CanReply: DefaultCanReplyFor(VisibilityPublic), + CanAnnounce: DefaultCanAnnounceFor(VisibilityPublic), +} + +// Returns a default interaction policy // for a post with visibility of public. func DefaultInteractionPolicyPublic() *InteractionPolicy { - return defaultPolicyPublic + // Copy global. + c := new(InteractionPolicy) + *c = *defaultPolicyPublic + return c } -// Returns the default interaction policy +// Returns a default interaction policy // for a post with visibility of unlocked. func DefaultInteractionPolicyUnlocked() *InteractionPolicy { // Same as public (for now). @@ -266,71 +386,31 @@ func DefaultInteractionPolicyUnlocked() *InteractionPolicy { } var defaultPolicyFollowersOnly = &InteractionPolicy{ - CanLike: PolicyRules{ - // Self, followers and - // mentioned can like. - Always: PolicyValues{ - PolicyValueAuthor, - PolicyValueFollowers, - PolicyValueMentioned, - }, - WithApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Self, followers and - // mentioned can reply. - Always: PolicyValues{ - PolicyValueAuthor, - PolicyValueFollowers, - PolicyValueMentioned, - }, - WithApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Only self can announce. - Always: PolicyValues{ - PolicyValueAuthor, - }, - WithApproval: make(PolicyValues, 0), - }, + CanLike: DefaultCanLikeFor(VisibilityFollowersOnly), + CanReply: DefaultCanReplyFor(VisibilityFollowersOnly), + CanAnnounce: DefaultCanAnnounceFor(VisibilityFollowersOnly), } -// Returns the default interaction policy for +// Returns a default interaction policy for // a post with visibility of followers only. func DefaultInteractionPolicyFollowersOnly() *InteractionPolicy { - return defaultPolicyFollowersOnly + // Copy global. + c := new(InteractionPolicy) + *c = *defaultPolicyFollowersOnly + return c } var defaultPolicyDirect = &InteractionPolicy{ - CanLike: PolicyRules{ - // Mentioned and self - // can always like. - Always: PolicyValues{ - PolicyValueAuthor, - PolicyValueMentioned, - }, - WithApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Mentioned and self - // can always reply. - Always: PolicyValues{ - PolicyValueAuthor, - PolicyValueMentioned, - }, - WithApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Only self can announce. - Always: PolicyValues{ - PolicyValueAuthor, - }, - WithApproval: make(PolicyValues, 0), - }, + CanLike: DefaultCanLikeFor(VisibilityDirect), + CanReply: DefaultCanReplyFor(VisibilityDirect), + CanAnnounce: DefaultCanAnnounceFor(VisibilityDirect), } -// Returns the default interaction policy +// Returns a default interaction policy // for a post with visibility of direct. func DefaultInteractionPolicyDirect() *InteractionPolicy { - return defaultPolicyDirect + // Copy global. + c := new(InteractionPolicy) + *c = *defaultPolicyDirect + return c } diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go index daf64a0d2..530a184d0 100644 --- a/internal/typeutils/frontendtointernal.go +++ b/internal/typeutils/frontendtointernal.go @@ -227,15 +227,15 @@ func APIInteractionPolicyToInteractionPolicy( } return >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: canLikeAlways, WithApproval: canLikeWithApproval, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: canReplyAlways, WithApproval: canReplyWithApproval, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: canAnnounceAlways, WithApproval: canAnnounceWithApproval, }, diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index f21bef83f..678b23ca5 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -1893,6 +1893,19 @@ func (c *Converter) InteractionPolicyToASInteractionPolicy( ) (vocab.GoToSocialInteractionPolicy, error) { policy := streams.NewGoToSocialInteractionPolicy() + /* + Implementation note for the below: + While it's possible for remote instances to set + sub-policies like canLike, canReply, etc to null + values, or omit them entirely, GtS always falls + back to default non-nil sub-policies when storing + policies created for local statuses. Therefore, + since we only ever serialize our *own* statuses + to AS format using this function, it's safe to + assume that the values will always be set, rather + than checking for nil ptrs. + */ + /* CAN LIKE */ diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index ffd971040..311835168 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -2867,7 +2867,10 @@ func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme // into an apimodel interaction policy. // // Provided status can be nil to convert a -// policy without a particular status in mind. +// policy without a particular status in mind, +// but ***if status is nil then sub-policies +// CanLike, CanReply, and CanAnnounce on +// the given policy must *not* be nil.*** // // RequestingAccount can also be nil for // unauthorized requests (web, public api etc). @@ -2877,19 +2880,54 @@ func (c *Converter) InteractionPolicyToAPIInteractionPolicy( status *gtsmodel.Status, requester *gtsmodel.Account, ) (*apimodel.InteractionPolicy, error) { - apiPolicy := &apimodel.InteractionPolicy{ - CanFavourite: apimodel.PolicyRules{ + apiPolicy := new(apimodel.InteractionPolicy) + + // gtsmodel CanLike -> apimodel CanFavourite + if policy.CanLike != nil { + // Use the set CanLike value. + apiPolicy.CanFavourite = apimodel.PolicyRules{ Always: policyValsToAPIPolicyVals(policy.CanLike.Always), WithApproval: policyValsToAPIPolicyVals(policy.CanLike.WithApproval), - }, - CanReply: apimodel.PolicyRules{ + } + } else { + // Use default CanLike value for this vis. + pCanLike := gtsmodel.DefaultCanLikeFor(status.Visibility) + apiPolicy.CanFavourite = apimodel.PolicyRules{ + Always: policyValsToAPIPolicyVals(pCanLike.Always), + WithApproval: policyValsToAPIPolicyVals(pCanLike.WithApproval), + } + } + + // gtsmodel CanReply -> apimodel CanReply + if policy.CanReply != nil { + // Use the set CanReply value. + apiPolicy.CanReply = apimodel.PolicyRules{ Always: policyValsToAPIPolicyVals(policy.CanReply.Always), WithApproval: policyValsToAPIPolicyVals(policy.CanReply.WithApproval), - }, - CanReblog: apimodel.PolicyRules{ + } + } else { + // Use default CanReply value for this vis. + pCanReply := gtsmodel.DefaultCanReplyFor(status.Visibility) + apiPolicy.CanReply = apimodel.PolicyRules{ + Always: policyValsToAPIPolicyVals(pCanReply.Always), + WithApproval: policyValsToAPIPolicyVals(pCanReply.WithApproval), + } + } + + // gtsmodel CanAnnounce -> apimodel CanReblog + if policy.CanAnnounce != nil { + // Use the set CanAnnounce value. + apiPolicy.CanReblog = apimodel.PolicyRules{ Always: policyValsToAPIPolicyVals(policy.CanAnnounce.Always), WithApproval: policyValsToAPIPolicyVals(policy.CanAnnounce.WithApproval), - }, + } + } else { + // Use default CanAnnounce value for this vis. + pCanAnnounce := gtsmodel.DefaultCanAnnounceFor(status.Visibility) + apiPolicy.CanReblog = apimodel.PolicyRules{ + Always: policyValsToAPIPolicyVals(pCanAnnounce.Always), + WithApproval: policyValsToAPIPolicyVals(pCanAnnounce.WithApproval), + } } if status == nil || requester == nil { diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 7d868ce4d..8b8ea7104 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -2238,13 +2238,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, }, @@ -2419,13 +2419,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, }, @@ -2451,14 +2451,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, WithApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, }, @@ -2483,13 +2483,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(false), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ Always: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, }, diff --git a/testrig/teststructs.go b/testrig/teststructs.go index edba34263..919d4c443 100644 --- a/testrig/teststructs.go +++ b/testrig/teststructs.go @@ -49,6 +49,7 @@ type TestStructs struct { EmailSender email.Sender WebPushSender *WebPushMockSender TransportController transport.Controller + InteractionFilter *interaction.Filter } func SetupTestStructs( @@ -122,6 +123,7 @@ func SetupTestStructs( EmailSender: emailSender, WebPushSender: webPushSender, TransportController: transportController, + InteractionFilter: intFilter, } }