diff options
Diffstat (limited to 'internal/typeutils')
| -rw-r--r-- | internal/typeutils/frontendtointernal.go | 172 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend.go | 123 | ||||
| -rw-r--r-- | internal/typeutils/internaltofrontend_test.go | 224 | 
3 files changed, 514 insertions, 5 deletions
diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go index f194770df..8ced14d58 100644 --- a/internal/typeutils/frontendtointernal.go +++ b/internal/typeutils/frontendtointernal.go @@ -18,6 +18,10 @@  package typeutils  import ( +	"fmt" +	"net/url" +	"slices" +  	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  ) @@ -57,3 +61,171 @@ func APIFilterActionToFilterAction(m apimodel.FilterAction) gtsmodel.FilterActio  	}  	return gtsmodel.FilterActionNone  } + +func APIPolicyValueToPolicyValue(u apimodel.PolicyValue) (gtsmodel.PolicyValue, error) { +	switch u { +	case apimodel.PolicyValuePublic: +		return gtsmodel.PolicyValuePublic, nil + +	case apimodel.PolicyValueFollowers: +		return gtsmodel.PolicyValueFollowers, nil + +	case apimodel.PolicyValueFollowing: +		return gtsmodel.PolicyValueFollowing, nil + +	case apimodel.PolicyValueMutuals: +		return gtsmodel.PolicyValueMutuals, nil + +	case apimodel.PolicyValueMentioned: +		return gtsmodel.PolicyValueMentioned, nil + +	case apimodel.PolicyValueAuthor: +		return gtsmodel.PolicyValueAuthor, nil + +	case apimodel.PolicyValueMe: +		err := fmt.Errorf("policyURI %s has no corresponding internal model", apimodel.PolicyValueMe) +		return "", err + +	default: +		// Parse URI to ensure it's a +		// url with a valid protocol. +		url, err := url.Parse(string(u)) +		if err != nil { +			err := fmt.Errorf("could not parse non-predefined policy value as uri: %w", err) +			return "", err +		} + +		if url.Host != "http" && url.Host != "https" { +			err := fmt.Errorf("non-predefined policy values must have protocol 'http' or 'https' (%s)", u) +			return "", err +		} + +		return gtsmodel.PolicyValue(u), nil +	} +} + +func APIInteractionPolicyToInteractionPolicy( +	p *apimodel.InteractionPolicy, +	v apimodel.Visibility, +) (*gtsmodel.InteractionPolicy, error) { +	visibility := APIVisToVis(v) + +	convertURIs := func(apiURIs []apimodel.PolicyValue) (gtsmodel.PolicyValues, error) { +		policyURIs := gtsmodel.PolicyValues{} +		for _, apiURI := range apiURIs { +			uri, err := APIPolicyValueToPolicyValue(apiURI) +			if err != nil { +				return nil, err +			} + +			if !uri.FeasibleForVisibility(visibility) { +				err := fmt.Errorf("policyURI %s is not feasible for visibility %s", apiURI, v) +				return nil, err +			} + +			policyURIs = append(policyURIs, uri) +		} +		return policyURIs, nil +	} + +	canLikeAlways, err := convertURIs(p.CanFavourite.Always) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_favourite.always: %w", v, err) +		return nil, err +	} + +	canLikeWithApproval, err := convertURIs(p.CanFavourite.WithApproval) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_favourite.with_approval: %w", v, err) +		return nil, err +	} + +	canReplyAlways, err := convertURIs(p.CanReply.Always) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_reply.always: %w", v, err) +		return nil, err +	} + +	canReplyWithApproval, err := convertURIs(p.CanReply.WithApproval) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_reply.with_approval: %w", v, err) +		return nil, err +	} + +	canAnnounceAlways, err := convertURIs(p.CanReblog.Always) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_reblog.always: %w", v, err) +		return nil, err +	} + +	canAnnounceWithApproval, err := convertURIs(p.CanReblog.WithApproval) +	if err != nil { +		err := fmt.Errorf("error converting %s.can_reblog.with_approval: %w", v, err) +		return nil, err +	} + +	// Normalize URIs. +	// +	// 1. Ensure canLikeAlways, canReplyAlways, +	//    and canAnnounceAlways include self +	//    (either explicitly or within public). + +	// ensureIncludesSelf adds the "author" PolicyValue +	// to given slice of PolicyValues, if not already +	// explicitly or implicitly included. +	ensureIncludesSelf := func(vals gtsmodel.PolicyValues) gtsmodel.PolicyValues { +		includesSelf := slices.ContainsFunc( +			vals, +			func(uri gtsmodel.PolicyValue) bool { +				return uri == gtsmodel.PolicyValuePublic || +					uri == gtsmodel.PolicyValueAuthor +			}, +		) + +		if includesSelf { +			// This slice of policy values +			// already includes self explicitly +			// or implicitly, nothing to change. +			return vals +		} + +		// Need to add self/author to +		// this slice of policy values. +		vals = append(vals, gtsmodel.PolicyValueAuthor) +		return vals +	} + +	canLikeAlways = ensureIncludesSelf(canLikeAlways) +	canReplyAlways = ensureIncludesSelf(canReplyAlways) +	canAnnounceAlways = ensureIncludesSelf(canAnnounceAlways) + +	// 2. Ensure canReplyAlways includes mentioned +	//    accounts (either explicitly or within public). +	if !slices.ContainsFunc( +		canReplyAlways, +		func(uri gtsmodel.PolicyValue) bool { +			return uri == gtsmodel.PolicyValuePublic || +				uri == gtsmodel.PolicyValueMentioned +		}, +	) { +		canReplyAlways = append( +			canReplyAlways, +			gtsmodel.PolicyValueMentioned, +		) +	} + +	return >smodel.InteractionPolicy{ +		CanLike: gtsmodel.PolicyRules{ +			Always:       canLikeAlways, +			WithApproval: canLikeWithApproval, +		}, +		CanReply: gtsmodel.PolicyRules{ +			Always:       canReplyAlways, +			WithApproval: canReplyWithApproval, +		}, +		CanAnnounce: gtsmodel.PolicyRules{ +			Always:       canAnnounceAlways, +			WithApproval: canAnnounceWithApproval, +		}, +	}, nil +} diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index d24ae3ea5..6350f3269 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -1234,6 +1234,20 @@ func (c *Converter) baseStatusToFrontend(  		log.Errorf(ctx, "error converting status emojis: %v", err)  	} +	// Take status's interaction policy, or +	// fall back to default for its visibility. +	var p *gtsmodel.InteractionPolicy +	if s.InteractionPolicy != nil { +		p = s.InteractionPolicy +	} else { +		p = gtsmodel.DefaultInteractionPolicyFor(s.Visibility) +	} + +	apiInteractionPolicy, err := c.InteractionPolicyToAPIInteractionPolicy(ctx, p, s, requestingAccount) +	if err != nil { +		return nil, gtserror.Newf("error converting interaction policy: %w", err) +	} +  	apiStatus := &apimodel.Status{  		ID:                 s.ID,  		CreatedAt:          util.FormatISO8601(s.CreatedAt), @@ -1258,6 +1272,7 @@ func (c *Converter) baseStatusToFrontend(  		Emojis:             apiEmojis,  		Card:               nil, // TODO: implement cards  		Text:               s.Text, +		InteractionPolicy:  *apiInteractionPolicy,  	}  	// Nullable fields. @@ -2256,3 +2271,111 @@ func (c *Converter) ThemesToAPIThemes(themes []*gtsmodel.Theme) []apimodel.Theme  	}  	return apiThemes  } + +// Convert the given gtsmodel policy +// into an apimodel interaction policy. +// +// Provided status can be nil to convert a +// policy without a particular status in mind. +// +// RequestingAccount can also be nil for +// unauthorized requests (web, public api etc). +func (c *Converter) InteractionPolicyToAPIInteractionPolicy( +	ctx context.Context, +	policy *gtsmodel.InteractionPolicy, +	_ *gtsmodel.Status, // Used in upcoming PR. +	_ *gtsmodel.Account, // Used in upcoming PR. +) (*apimodel.InteractionPolicy, error) { +	apiPolicy := &apimodel.InteractionPolicy{ +		CanFavourite: apimodel.PolicyRules{ +			Always:       policyValsToAPIPolicyVals(policy.CanLike.Always), +			WithApproval: policyValsToAPIPolicyVals(policy.CanLike.WithApproval), +		}, +		CanReply: apimodel.PolicyRules{ +			Always:       policyValsToAPIPolicyVals(policy.CanReply.Always), +			WithApproval: policyValsToAPIPolicyVals(policy.CanReply.WithApproval), +		}, +		CanReblog: apimodel.PolicyRules{ +			Always:       policyValsToAPIPolicyVals(policy.CanAnnounce.Always), +			WithApproval: policyValsToAPIPolicyVals(policy.CanAnnounce.WithApproval), +		}, +	} + +	return apiPolicy, nil +} + +func policyValsToAPIPolicyVals(vals gtsmodel.PolicyValues) []apimodel.PolicyValue { + +	var ( +		valsLen = len(vals) + +		// Use a map to deduplicate added vals as we go. +		addedVals = make(map[apimodel.PolicyValue]struct{}, valsLen) + +		// Vals we'll be returning. +		apiVals = make([]apimodel.PolicyValue, 0, valsLen) +	) + +	for _, policyVal := range vals { +		switch policyVal { + +		case gtsmodel.PolicyValueAuthor: +			// Author can do this. +			newVal := apimodel.PolicyValueAuthor +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		case gtsmodel.PolicyValueMentioned: +			// Mentioned can do this. +			newVal := apimodel.PolicyValueMentioned +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		case gtsmodel.PolicyValueMutuals: +			// Mutuals can do this. +			newVal := apimodel.PolicyValueMutuals +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		case gtsmodel.PolicyValueFollowing: +			// Following can do this. +			newVal := apimodel.PolicyValueFollowing +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		case gtsmodel.PolicyValueFollowers: +			// Followers can do this. +			newVal := apimodel.PolicyValueFollowers +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		case gtsmodel.PolicyValuePublic: +			// Public can do this. +			newVal := apimodel.PolicyValuePublic +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} + +		default: +			// Specific URI of ActivityPub Actor. +			newVal := apimodel.PolicyValue(policyVal) +			if _, added := addedVals[newVal]; !added { +				apiVals = append(apiVals, newVal) +				addedVals[newVal] = struct{}{} +			} +		} +	} + +	return apiVals +} diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go index c4da0d57c..9fd4cea46 100644 --- a/internal/typeutils/internaltofrontend_test.go +++ b/internal/typeutils/internaltofrontend_test.go @@ -546,7 +546,27 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {    ],    "card": null,    "poll": null, -  "text": "hello world! #welcome ! first post on the instance :rainbow: !" +  "text": "hello world! #welcome ! first post on the instance :rainbow: !", +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    } +  }  }`, string(b))  } @@ -701,7 +721,27 @@ func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() {        ],        "status_matches": []      } -  ] +  ], +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    } +  }  }`, string(b))  } @@ -877,7 +917,27 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments    "tags": [],    "emojis": [],    "card": null, -  "poll": null +  "poll": null, +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    } +  }  }`, string(b))  } @@ -955,6 +1015,26 @@ func (suite *InternalToFrontendTestSuite) TestStatusToWebStatus() {    "emojis": [],    "card": null,    "poll": null, +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    } +  },    "media_attachments": [      {        "id": "01HE7Y3C432WRSNS10EZM86SA5", @@ -1137,7 +1217,121 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()    ],    "card": null,    "poll": null, -  "text": "hello world! #welcome ! first post on the instance :rainbow: !" +  "text": "hello world! #welcome ! first post on the instance :rainbow: !", +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "public" +      ], +      "with_approval": [] +    } +  } +}`, string(b)) +} + +func (suite *InternalToFrontendTestSuite) TestStatusToFrontendPartialInteractions() { +	testStatus := >smodel.Status{} +	*testStatus = *suite.testStatuses["local_account_1_status_3"] +	testStatus.Language = "" +	requestingAccount := suite.testAccounts["admin_account"] +	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil, nil) +	suite.NoError(err) + +	b, err := json.MarshalIndent(apiStatus, "", "  ") +	suite.NoError(err) + +	suite.Equal(`{ +  "id": "01F8MHBBN8120SYH7D5S050MGK", +  "created_at": "2021-10-20T10:40:37.000Z", +  "in_reply_to_id": null, +  "in_reply_to_account_id": null, +  "sensitive": false, +  "spoiler_text": "test: you shouldn't be able to interact with this post in any way", +  "visibility": "private", +  "language": null, +  "uri": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHBBN8120SYH7D5S050MGK", +  "url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHBBN8120SYH7D5S050MGK", +  "replies_count": 0, +  "reblogs_count": 0, +  "favourites_count": 0, +  "favourited": false, +  "reblogged": false, +  "muted": false, +  "bookmarked": false, +  "pinned": false, +  "content": "this is a very personal post that I don't want anyone to interact with at all, and i only want mutuals to see it", +  "reblog": null, +  "application": { +    "name": "really cool gts application", +    "website": "https://reallycool.app" +  }, +  "account": { +    "id": "01F8MH1H7YV1Z7D2C8K2730QBF", +    "username": "the_mighty_zork", +    "acct": "the_mighty_zork", +    "display_name": "original zork (he/they)", +    "locked": false, +    "discoverable": true, +    "bot": false, +    "created_at": "2022-05-20T11:09:18.000Z", +    "note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e", +    "url": "http://localhost:8080/@the_mighty_zork", +    "avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg", +    "avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg", +    "avatar_description": "a green goblin looking nasty", +    "header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", +    "header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg", +    "header_description": "A very old-school screenshot of the original team fortress mod for quake", +    "followers_count": 2, +    "following_count": 2, +    "statuses_count": 8, +    "last_status_at": "2024-01-10T09:24:00.000Z", +    "emojis": [], +    "fields": [], +    "enable_rss": true, +    "role": { +      "name": "user" +    } +  }, +  "media_attachments": [], +  "mentions": [], +  "tags": [], +  "emojis": [], +  "card": null, +  "poll": null, +  "text": "this is a very personal post that I don't want anyone to interact with at all, and i only want mutuals to see it", +  "interaction_policy": { +    "can_favourite": { +      "always": [ +        "author" +      ], +      "with_approval": [] +    }, +    "can_reply": { +      "always": [ +        "author" +      ], +      "with_approval": [] +    }, +    "can_reblog": { +      "always": [ +        "author" +      ], +      "with_approval": [] +    } +  }  }`, string(b))  } @@ -2014,7 +2208,27 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {        "tags": [],        "emojis": [],        "card": null, -      "poll": null +      "poll": null, +      "interaction_policy": { +        "can_favourite": { +          "always": [ +            "public" +          ], +          "with_approval": [] +        }, +        "can_reply": { +          "always": [ +            "public" +          ], +          "with_approval": [] +        }, +        "can_reblog": { +          "always": [ +            "public" +          ], +          "with_approval": [] +        } +      }      }    ],    "rules": [  | 
