gotosocial/internal/gtsmodel/interactionpolicy.go
tobi 259fa1ffac [bugfix] Update interaction policies of freshly dereffed statuses if different from last deref (#4474)
# Description

> 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.

This pull request adds a check to see whether interaction policy on a refreshed status is different from the interaction policy set on that status before, and updates the status with the new policy if it's changed.

Should fix a pesky issue where folks on v0.19.2 and above still can't interact with statuses they dereferenced before updating.

## Checklist

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.
- [ ] 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/4474
Reviewed-by: kim <gruf@noreply.codeberg.org>
Co-authored-by: tobi <tobi.smethurst@protonmail.com>
Co-committed-by: tobi <tobi.smethurst@protonmail.com>
2025-10-05 13:33:16 +02:00

497 lines
14 KiB
Go

// 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 gtsmodel
import "slices"
// A policy URI is GoToSocial's internal representation of
// one ActivityPub URI for an Actor or a Collection of Actors,
// specific to the domain of enforcing interaction policies.
//
// A PolicyValue can be stored in the database either as one
// of the Value constants defined below (to save space), OR as
// a full-fledged ActivityPub URI.
//
// A PolicyValue should be translated to the canonical string
// value of the represented URI when federating an item, or
// from the canonical string value of the URI when receiving
// or retrieving an item.
//
// For example, if the PolicyValue `followers` was being
// federated outwards in an interaction policy attached to an
// item created by the actor `https://example.org/users/someone`,
// then it should be translated to their followers URI when sent,
// eg., `https://example.org/users/someone/followers`.
//
// Likewise, if GoToSocial receives an item with an interaction
// policy containing `https://example.org/users/someone/followers`,
// and the item was created by `https://example.org/users/someone`,
// then the followers URI would be converted to `followers`
// for internal storage.
type PolicyValue string
const (
// Stand-in for ActivityPub magic public URI,
// which encompasses every possible Actor URI.
PolicyValuePublic PolicyValue = "public"
// Stand-in for the Followers Collection of
// the item owner's Actor.
PolicyValueFollowers PolicyValue = "followers"
// Stand-in for the Following Collection of
// the item owner's Actor.
PolicyValueFollowing PolicyValue = "following"
// Stand-in for the Mutuals Collection of
// the item owner's Actor.
//
// (TODO: Reserved, currently unused).
PolicyValueMutuals PolicyValue = "mutuals"
// Stand-in for Actor URIs tagged in the item.
PolicyValueMentioned PolicyValue = "mentioned"
// Stand-in for the Actor URI of the item owner.
PolicyValueAuthor PolicyValue = "author"
)
// FeasibleForVisibility returns true if the PolicyValue could feasibly
// be set in a policy for an item with the given visibility, otherwise
// returns false.
//
// For example, PolicyValuePublic could not be set in a policy for an
// item with visibility FollowersOnly, but could be set in a policy
// for an item with visibility Public or Unlocked.
//
// This is not prescriptive, and should be used only to guide policy
// choices. Eg., if a remote instance wants to do something wacky like
// set "anyone can interact with this status" for a Direct visibility
// status, that's their business; our normal visibility filtering will
// prevent users on our instance from actually being able to interact
// unless they can see the status anyway.
func (p PolicyValue) FeasibleForVisibility(v Visibility) bool {
switch p {
// Mentioned and self Values are
// feasible for any visibility.
case PolicyValueAuthor,
PolicyValueMentioned:
return true
// Followers/following/mutual Values
// are only feasible for items with
// followers visibility and higher.
case PolicyValueFollowers,
PolicyValueFollowing:
return v == VisibilityFollowersOnly ||
v == VisibilityPublic ||
v == VisibilityUnlocked
// Public policy Value only feasible
// for items that are To or CC public.
case PolicyValuePublic:
return v == VisibilityUnlocked ||
v == VisibilityPublic
// Any other combo
// is probably fine.
default:
return true
}
}
type PolicyValues []PolicyValue
// PolicyPermission represents the permission
// state for a certain Actor URI and interaction
// type, in relation to a policy.
type PolicyPermission int
const (
// Interaction is forbidden for this
// PolicyValue + interaction combination.
PolicyPermissionForbidden PolicyPermission = iota
// Interaction is conditionally permitted
// for this PolicyValue + interaction combo,
// pending approval by the item owner.
PolicyPermissionManualApproval
// Interaction is permitted for this
// PolicyValue + interaction combination.
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.
type PolicyCheckResult struct {
// Permission permitted /
// with approval / forbidden.
Permission PolicyPermission
// Value that this check matched on.
// Only set if Permission = automatic.
PermissionMatchedOn *PolicyValue
}
// MatchedOnCollection returns true if this policy check
// result turned up AutomaticApproval, and matched based on the
// requester's presence in a followers or following collection.
func (pcr *PolicyCheckResult) MatchedOnCollection() bool {
if !pcr.AutomaticApproval() {
// Not permitted at all
// so definitely didn't
// match on collection.
return false
}
if pcr.PermissionMatchedOn == nil {
return false
}
return *pcr.PermissionMatchedOn == PolicyValueFollowers ||
*pcr.PermissionMatchedOn == PolicyValueFollowing
}
// AutomaticApproval returns true if this policy
// check resulted in Permission = automatic approval.
func (pcr *PolicyCheckResult) AutomaticApproval() bool {
return pcr.Permission == PolicyPermissionAutomaticApproval
}
// Permitted returns true if this policy
// check resulted in Permission = manual approval.
func (pcr *PolicyCheckResult) ManualApproval() bool {
return pcr.Permission == PolicyPermissionManualApproval
}
// Permitted returns true if this policy
// check resulted in Permission = forbidden.
func (pcr *PolicyCheckResult) Forbidden() bool {
return pcr.Permission == PolicyPermissionForbidden
}
// An InteractionPolicy determines which
// interactions will be accepted for an
// item, and according to what rules.
type InteractionPolicy struct {
// Conditions in which a Like
// interaction will be accepted
// for an item with this policy.
CanLike *PolicyRules
// Conditions in which a Reply
// interaction will be accepted
// for an item with this policy.
CanReply *PolicyRules
// Conditions in which an Announce
// interaction will be accepted
// for an item with this policy.
CanAnnounce *PolicyRules
}
// PolicyRules represents the rules according
// to which a certain interaction is permitted
// to various Actor and Actor Collection URIs.
type PolicyRules struct {
// AutomaticApproval is for PolicyValue entries
// that are pre-approved to do an interaction
// and will receive automatic approval.
//
// Note: This is stored in the db as JSON.
// JSON tags set for back compat with previous
// (now deprecated) name for this PolicyValues.
AutomaticApproval PolicyValues `json:"Always,omitempty"`
// ManualApproval is for PolicyValues entries
// that are conditionally permitted to do
// an interaction, pending manual approval.
//
// Note: This is stored in the db as JSON.
// JSON tags set for back compat with previous
// (now deprecated) name for this PolicyValues.
ManualApproval PolicyValues `json:"WithApproval,omitempty"`
}
// DifferentFrom returns true if pr1 and pr2
// are not equal in terms of nilness or content.
func (pr1 *PolicyRules) DifferentFrom(pr2 *PolicyRules) bool {
// If one PolicyRules is nil and
// the other isn't, they're different.
if pr1 == nil && pr2 != nil ||
pr1 != nil && pr2 == nil {
return true
}
// Check if AutomaticApproval
// differs between the two.
if slices.Compare(
pr1.AutomaticApproval,
pr2.AutomaticApproval,
) != 0 {
return true
}
// Check if ManualApproval
// differs between the two.
if slices.Compare(
pr1.ManualApproval,
pr2.ManualApproval,
) != 0 {
return true
}
// They're the
// same picture.
return false
}
// Returns the default interaction policy
// for the given visibility level.
func DefaultInteractionPolicyFor(v Visibility) *InteractionPolicy {
switch v {
case VisibilityPublic:
return DefaultInteractionPolicyPublic()
case VisibilityUnlocked:
return DefaultInteractionPolicyUnlocked()
case VisibilityFollowersOnly, VisibilityMutualsOnly:
return DefaultInteractionPolicyFollowersOnly()
case VisibilityDirect:
return DefaultInteractionPolicyDirect()
default:
panic("invalid visibility")
}
}
// 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")
}
}
// 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 {
// Copy global.
c := new(InteractionPolicy)
*c = *defaultPolicyPublic
return c
}
// Returns a default interaction policy
// for a post with visibility of unlocked.
func DefaultInteractionPolicyUnlocked() *InteractionPolicy {
// Same as public (for now).
return defaultPolicyPublic
}
var defaultPolicyFollowersOnly = &InteractionPolicy{
CanLike: DefaultCanLikeFor(VisibilityFollowersOnly),
CanReply: DefaultCanReplyFor(VisibilityFollowersOnly),
CanAnnounce: DefaultCanAnnounceFor(VisibilityFollowersOnly),
}
// Returns a default interaction policy for
// a post with visibility of followers only.
func DefaultInteractionPolicyFollowersOnly() *InteractionPolicy {
// Copy global.
c := new(InteractionPolicy)
*c = *defaultPolicyFollowersOnly
return c
}
var defaultPolicyDirect = &InteractionPolicy{
CanLike: DefaultCanLikeFor(VisibilityDirect),
CanReply: DefaultCanReplyFor(VisibilityDirect),
CanAnnounce: DefaultCanAnnounceFor(VisibilityDirect),
}
// Returns a default interaction policy
// for a post with visibility of direct.
func DefaultInteractionPolicyDirect() *InteractionPolicy {
// Copy global.
c := new(InteractionPolicy)
*c = *defaultPolicyDirect
return c
}
// DifferentFrom returns true if p1 and p2 are different.
func (ip1 *InteractionPolicy) DifferentFrom(ip2 *InteractionPolicy) bool {
// If one policy is null and the
// other isn't, they're different.
if ip1 == nil && ip2 != nil ||
ip1 != nil && ip2 == nil {
return true
}
// If they're both nil we don't
// need to check anything else.
if ip1 == nil && ip2 == nil {
return false
}
// If CanLike differs from one policy
// to the next, they're different.
if ip1.CanLike.DifferentFrom(ip2.CanLike) {
return true
}
// If CanReply differs from one policy
// to the next, they're different.
if ip1.CanReply.DifferentFrom(ip2.CanReply) {
return true
}
// If CanAnnounce differs from one policy
// to the next, they're different.
if ip1.CanAnnounce.DifferentFrom(ip2.CanAnnounce) {
return true
}
// Looks the
// same chief.
return false
}