diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 344e0fdb3..8dbb903a3 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -1113,6 +1113,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, @@ -1139,6 +1142,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), @@ -1146,67 +1151,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 extractPolicyRules(propIter.Get(), owner) -} - -func extractCanReply( - prop vocab.GoToSocialCanReplyProperty, - owner *gtsmodel.Account, -) gtsmodel.PolicyRules { - if prop == nil || prop.Len() != 1 { - return gtsmodel.PolicyRules{} - } - - propIter := prop.At(0) - if !propIter.IsGoToSocialCanReply() { - return gtsmodel.PolicyRules{} - } - - return extractPolicyRules(propIter.Get(), owner) -} - -func extractCanAnnounce( - prop vocab.GoToSocialCanAnnounceProperty, - owner *gtsmodel.Account, -) gtsmodel.PolicyRules { - if prop == nil || prop.Len() != 1 { - return gtsmodel.PolicyRules{} - } - - propIter := prop.At(0) - if !propIter.IsGoToSocialCanAnnounce() { - return gtsmodel.PolicyRules{} + return nil } withRules := propIter.Get() if withRules == nil { - return gtsmodel.PolicyRules{} + return nil } - return extractPolicyRules(propIter.Get(), owner) + return extractPolicyRules(withRules, 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 { + if prop == nil || prop.Len() != 1 { + return nil + } + + propIter := prop.At(0) + if !propIter.IsGoToSocialCanReply() { + return nil + } + + withRules := propIter.Get() + if withRules == nil { + return nil + } + + return extractPolicyRules(withRules, 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 { + if prop == nil || prop.Len() != 1 { + return nil + } + + propIter := prop.At(0) + if !propIter.IsGoToSocialCanAnnounce() { + return nil + } + + withRules := propIter.Get() + if withRules == nil { + return nil + } + + return extractPolicyRules(withRules, owner) } func extractPolicyRules( withRules WithPolicyRules, owner *gtsmodel.Account, -) gtsmodel.PolicyRules { - if withRules == nil { - return gtsmodel.PolicyRules{} - } - +) *gtsmodel.PolicyRules { // Check for `automaticApproval` and // `manualApproval` properties first. var ( @@ -1216,7 +1236,7 @@ func extractPolicyRules( if (automaticApproval != nil && automaticApproval.Len() != 0) || (manualApproval != nil && manualApproval.Len() != 0) { // At least one is set, use these props. - return gtsmodel.PolicyRules{ + return >smodel.PolicyRules{ AutomaticApproval: extractPolicyValues(automaticApproval, owner), ManualApproval: extractPolicyValues(manualApproval, owner), } @@ -1226,7 +1246,7 @@ func extractPolicyRules( // and `withApproval` properties. // // TODO: Remove this in GtS v0.21.0. - return gtsmodel.PolicyRules{ + return >smodel.PolicyRules{ AutomaticApproval: extractPolicyValues(withRules.GetGoToSocialAlways(), owner), ManualApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), owner), } diff --git a/internal/ap/extractpolicy_test.go b/internal/ap/extractpolicy_test.go index 24b198b29..85221dcef 100644 --- a/internal/ap/extractpolicy_test.go +++ b/internal/ap/extractpolicy_test.go @@ -102,13 +102,13 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicy() { ) expectedPolicy := >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValuePublic, }, ManualApproval: gtsmodel.PolicyValues{}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, gtsmodel.PolicyValueFollowers, @@ -119,7 +119,75 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicy() { gtsmodel.PolicyValuePublic, }, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ + AutomaticApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValueAuthor, + }, + ManualApproval: gtsmodel.PolicyValues{ + gtsmodel.PolicyValuePublic, + }, + }, + } + suite.EqualValues(expectedPolicy, policy) +} + +func (suite *ExtractPolicyTestSuite) TestExtractPolicyUnsetProps() { + rawNote := `{ + "@context": [ + "https://gotosocial.org/ns", + "https://www.w3.org/ns/activitystreams" + ], + "content": "hey @f0x and @dumpsterqueer", + "contentMap": { + "en": "hey @f0x and @dumpsterqueer", + "fr": "bonjour @f0x et @dumpsterqueer" + }, + "interactionPolicy": { + "canAnnounce": { + "automaticApproval": [ + "http://localhost:8080/users/the_mighty_zork" + ], + "manualApproval": [ + "https://www.w3.org/ns/activitystreams#Public" + ] + } + }, + "tag": [ + { + "href": "https://gts.superseriousbusiness.org/users/dumpsterqueer", + "name": "@dumpsterqueer@superseriousbusiness.org", + "type": "Mention" + }, + { + "href": "https://gts.superseriousbusiness.org/users/f0x", + "name": "@f0x@superseriousbusiness.org", + "type": "Mention" + } + ], + "type": "Note" +}` + + statusable, err := ap.ResolveStatusable( + suite.T().Context(), + io.NopCloser( + bytes.NewBufferString(rawNote), + ), + ) + if err != nil { + suite.FailNow(err.Error()) + } + + policy := ap.ExtractInteractionPolicy( + statusable, + // Zork didn't actually create + // this status but nevermind. + suite.testAccounts["local_account_1"], + ) + + expectedPolicy := >smodel.InteractionPolicy{ + CanLike: nil, + CanReply: nil, + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, }, @@ -202,13 +270,13 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicyDeprecated() { ) expectedPolicy := >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValuePublic, }, ManualApproval: gtsmodel.PolicyValues{}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, gtsmodel.PolicyValueFollowers, @@ -219,7 +287,7 @@ func (suite *ExtractPolicyTestSuite) TestExtractPolicyDeprecated() { gtsmodel.PolicyValuePublic, }, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{ gtsmodel.PolicyValueAuthor, }, diff --git a/internal/filter/interaction/interactable.go b/internal/filter/interaction/interactable.go index 2052ac78e..38a43f23d 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.PolicyPermissionAutomaticApproval, @@ -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.PolicyPermissionAutomaticApproval, @@ -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.AutomaticApproval, @@ -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.ManualApproval, ) 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.PolicyPermissionAutomaticApproval, - PermissionMatchedOn: &matchAlwaysValue, + PermissionMatchedOn: &matchAutomaticValue, }, nil - case matchWithApproval == explicit: + case matchManual == explicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionManualApproval, }, nil // Then try implicit match, - // prioritizing "always". - case matchAlways == implicit: + // prioritizing automatic. + case matchAutomatic == implicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionAutomaticApproval, - PermissionMatchedOn: &matchAlwaysValue, + PermissionMatchedOn: &matchAutomaticValue, }, nil - case matchWithApproval == implicit: + case matchManual == implicit: return >smodel.PolicyCheckResult{ Permission: gtsmodel.PolicyPermissionManualApproval, }, 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 886c2e44b..0e248e49e 100644 --- a/internal/gtsmodel/interactionpolicy.go +++ b/internal/gtsmodel/interactionpolicy.go @@ -129,6 +129,19 @@ const ( PolicyPermissionAutomaticApproval ) +func (p PolicyPermission) String() string { + switch p { + case PolicyPermissionForbidden: + return "forbidden" + case PolicyPermissionManualApproval: + return "manualApproval" + case PolicyPermissionAutomaticApproval: + return "automaticApproval" + 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 @@ -236,37 +249,144 @@ func DefaultInteractionPolicyFor(v Visibility) *InteractionPolicy { } } -var defaultPolicyPublic = &InteractionPolicy{ - CanLike: PolicyRules{ - // Anyone can like. - AutomaticApproval: PolicyValues{ - PolicyValuePublic, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Anyone can reply. - AutomaticApproval: PolicyValues{ - PolicyValuePublic, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Anyone can announce. - AutomaticApproval: PolicyValues{ - PolicyValuePublic, - }, - ManualApproval: 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{ + AutomaticApproval: PolicyValues{ + PolicyValuePublic, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Self, followers and + // mentioned can like. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + PolicyValueFollowers, + PolicyValueMentioned, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Mentioned and self + // can always like. + case VisibilityDirect: + return &PolicyRules{ + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + PolicyValueMentioned, + }, + ManualApproval: 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{ + AutomaticApproval: PolicyValues{ + PolicyValuePublic, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Self, followers and + // mentioned can reply. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + PolicyValueFollowers, + PolicyValueMentioned, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Mentioned and self + // can always reply. + case VisibilityDirect: + return &PolicyRules{ + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + PolicyValueMentioned, + }, + ManualApproval: 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{ + AutomaticApproval: PolicyValues{ + PolicyValuePublic, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Only self can announce. + case VisibilityFollowersOnly, VisibilityMutualsOnly: + return &PolicyRules{ + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + }, + ManualApproval: make(PolicyValues, 0), + } + + // Only self can announce. + case VisibilityDirect: + return &PolicyRules{ + AutomaticApproval: PolicyValues{ + PolicyValueAuthor, + }, + ManualApproval: 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). @@ -274,71 +394,31 @@ func DefaultInteractionPolicyUnlocked() *InteractionPolicy { } var defaultPolicyFollowersOnly = &InteractionPolicy{ - CanLike: PolicyRules{ - // Self, followers and - // mentioned can like. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - PolicyValueFollowers, - PolicyValueMentioned, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Self, followers and - // mentioned can reply. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - PolicyValueFollowers, - PolicyValueMentioned, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Only self can announce. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - }, - ManualApproval: 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. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - PolicyValueMentioned, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanReply: PolicyRules{ - // Mentioned and self - // can always reply. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - PolicyValueMentioned, - }, - ManualApproval: make(PolicyValues, 0), - }, - CanAnnounce: PolicyRules{ - // Only self can announce. - AutomaticApproval: PolicyValues{ - PolicyValueAuthor, - }, - ManualApproval: 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 973b20632..239527eb1 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{ AutomaticApproval: canLikeAlways, ManualApproval: canLikeWithApproval, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: canReplyAlways, ManualApproval: canReplyWithApproval, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: canAnnounceAlways, ManualApproval: canAnnounceWithApproval, }, diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index cef48e194..4762e3c8b 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -1938,6 +1938,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 b6cb2bb37..bdb33243d 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -2782,7 +2782,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). @@ -2792,19 +2795,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{ AutomaticApproval: policyValsToAPIPolicyVals(policy.CanLike.AutomaticApproval), ManualApproval: policyValsToAPIPolicyVals(policy.CanLike.ManualApproval), - }, - CanReply: apimodel.PolicyRules{ + } + } else { + // Use default CanLike value for this vis. + pCanLike := gtsmodel.DefaultCanLikeFor(status.Visibility) + apiPolicy.CanFavourite = apimodel.PolicyRules{ + AutomaticApproval: policyValsToAPIPolicyVals(pCanLike.AutomaticApproval), + ManualApproval: policyValsToAPIPolicyVals(pCanLike.ManualApproval), + } + } + + // gtsmodel CanReply -> apimodel CanReply + if policy.CanReply != nil { + // Use the set CanReply value. + apiPolicy.CanReply = apimodel.PolicyRules{ AutomaticApproval: policyValsToAPIPolicyVals(policy.CanReply.AutomaticApproval), ManualApproval: policyValsToAPIPolicyVals(policy.CanReply.ManualApproval), - }, - CanReblog: apimodel.PolicyRules{ + } + } else { + // Use default CanReply value for this vis. + pCanReply := gtsmodel.DefaultCanReplyFor(status.Visibility) + apiPolicy.CanReply = apimodel.PolicyRules{ + AutomaticApproval: policyValsToAPIPolicyVals(pCanReply.AutomaticApproval), + ManualApproval: policyValsToAPIPolicyVals(pCanReply.ManualApproval), + } + } + + // gtsmodel CanAnnounce -> apimodel CanReblog + if policy.CanAnnounce != nil { + // Use the set CanAnnounce value. + apiPolicy.CanReblog = apimodel.PolicyRules{ AutomaticApproval: policyValsToAPIPolicyVals(policy.CanAnnounce.AutomaticApproval), ManualApproval: policyValsToAPIPolicyVals(policy.CanAnnounce.ManualApproval), - }, + } + } else { + // Use default CanAnnounce value for this vis. + pCanAnnounce := gtsmodel.DefaultCanAnnounceFor(status.Visibility) + apiPolicy.CanReblog = apimodel.PolicyRules{ + AutomaticApproval: policyValsToAPIPolicyVals(pCanAnnounce.AutomaticApproval), + ManualApproval: policyValsToAPIPolicyVals(pCanAnnounce.ManualApproval), + } } defer func() { diff --git a/testrig/testmodels.go b/testrig/testmodels.go index db221459b..86ea32fce 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -2245,13 +2245,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGY43H3N2C8EWPR2FPYEXG", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, }, @@ -2428,13 +2428,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, }, @@ -2460,14 +2460,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(true), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, ManualApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, }, @@ -2492,13 +2492,13 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedWithApplicationID: "01F8MGYG9E893WRHW0TAEXR8GJ", Federated: util.Ptr(false), InteractionPolicy: >smodel.InteractionPolicy{ - CanLike: gtsmodel.PolicyRules{ + CanLike: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanReply: gtsmodel.PolicyRules{ + CanReply: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValuePublic}, }, - CanAnnounce: gtsmodel.PolicyRules{ + CanAnnounce: >smodel.PolicyRules{ AutomaticApproval: gtsmodel.PolicyValues{gtsmodel.PolicyValueAuthor}, }, }, diff --git a/testrig/teststructs.go b/testrig/teststructs.go index f119bd113..a1e241f4e 100644 --- a/testrig/teststructs.go +++ b/testrig/teststructs.go @@ -50,6 +50,7 @@ type TestStructs struct { EmailSender email.Sender WebPushSender *WebPushMockSender TransportController transport.Controller + InteractionFilter *interaction.Filter } func SetupTestStructs( @@ -120,6 +121,7 @@ func SetupTestStructs( EmailSender: emailSender, WebPushSender: webPushSender, TransportController: transportController, + InteractionFilter: intFilter, } }