diff options
Diffstat (limited to 'internal/ap')
| -rw-r--r-- | internal/ap/ap_test.go | 63 | ||||
| -rw-r--r-- | internal/ap/extract.go | 131 | ||||
| -rw-r--r-- | internal/ap/extractpolicy_test.go | 137 | ||||
| -rw-r--r-- | internal/ap/interfaces.go | 44 | ||||
| -rw-r--r-- | internal/ap/normalize.go | 101 | ||||
| -rw-r--r-- | internal/ap/properties.go | 21 | ||||
| -rw-r--r-- | internal/ap/resolve.go | 55 | ||||
| -rw-r--r-- | internal/ap/serialize.go | 3 | 
8 files changed, 539 insertions, 16 deletions
| diff --git a/internal/ap/ap_test.go b/internal/ap/ap_test.go index 0a9f66ca6..f982e4443 100644 --- a/internal/ap/ap_test.go +++ b/internal/ap/ap_test.go @@ -28,6 +28,7 @@ import (  	"github.com/superseriousbusiness/activity/streams"  	"github.com/superseriousbusiness/activity/streams/vocab"  	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/testrig"  ) @@ -103,6 +104,66 @@ func noteWithMentions1() vocab.ActivityStreamsNote {  	note.SetActivityStreamsContent(content) +	policy := streams.NewGoToSocialInteractionPolicy() + +	// Set canLike. +	canLike := streams.NewGoToSocialCanLike() + +	// Anyone can like. +	canLikeAlwaysProp := streams.NewGoToSocialAlwaysProperty() +	canLikeAlwaysProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) +	canLike.SetGoToSocialAlways(canLikeAlwaysProp) + +	// Empty approvalRequired. +	canLikeApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty() +	canLike.SetGoToSocialApprovalRequired(canLikeApprovalRequiredProp) + +	// Set canLike on the policy. +	canLikeProp := streams.NewGoToSocialCanLikeProperty() +	canLikeProp.AppendGoToSocialCanLike(canLike) +	policy.SetGoToSocialCanLike(canLikeProp) + +	// Build canReply. +	canReply := streams.NewGoToSocialCanReply() + +	// Anyone can reply. +	canReplyAlwaysProp := streams.NewGoToSocialAlwaysProperty() +	canReplyAlwaysProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) +	canReply.SetGoToSocialAlways(canReplyAlwaysProp) + +	// Set empty approvalRequired. +	canReplyApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty() +	canReply.SetGoToSocialApprovalRequired(canReplyApprovalRequiredProp) + +	// Set canReply on the policy. +	canReplyProp := streams.NewGoToSocialCanReplyProperty() +	canReplyProp.AppendGoToSocialCanReply(canReply) +	policy.SetGoToSocialCanReply(canReplyProp) + +	// Build canAnnounce. +	canAnnounce := streams.NewGoToSocialCanAnnounce() + +	// Only f0x and dumpsterqueer can announce. +	canAnnounceAlwaysProp := streams.NewGoToSocialAlwaysProperty() +	canAnnounceAlwaysProp.AppendIRI(testrig.URLMustParse("https://gts.superseriousbusiness.org/users/dumpsterqueer")) +	canAnnounceAlwaysProp.AppendIRI(testrig.URLMustParse("https://gts.superseriousbusiness.org/users/f0x")) +	canAnnounce.SetGoToSocialAlways(canAnnounceAlwaysProp) + +	// Public requires approval to announce. +	canAnnounceApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty() +	canAnnounceApprovalRequiredProp.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) +	canAnnounce.SetGoToSocialApprovalRequired(canAnnounceApprovalRequiredProp) + +	// Set canAnnounce on the policy. +	canAnnounceProp := streams.NewGoToSocialCanAnnounceProperty() +	canAnnounceProp.AppendGoToSocialCanAnnounce(canAnnounce) +	policy.SetGoToSocialCanAnnounce(canAnnounceProp) + +	// Set the policy on the note. +	policyProp := streams.NewGoToSocialInteractionPolicyProperty() +	policyProp.AppendGoToSocialInteractionPolicy(policy) +	note.SetGoToSocialInteractionPolicy(policyProp) +  	return note  } @@ -296,6 +357,7 @@ type APTestSuite struct {  	addressable3      ap.Addressable  	addressable4      vocab.ActivityStreamsAnnounce  	addressable5      ap.Addressable +	testAccounts      map[string]*gtsmodel.Account  }  func (suite *APTestSuite) jsonToType(rawJson string) (vocab.Type, map[string]interface{}) { @@ -336,4 +398,5 @@ func (suite *APTestSuite) SetupTest() {  	suite.addressable3 = addressable3()  	suite.addressable4 = addressable4()  	suite.addressable5 = addressable5() +	suite.testAccounts = testrig.NewTestAccounts()  } diff --git a/internal/ap/extract.go b/internal/ap/extract.go index e0c90c5d7..ce1e2d421 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -1057,6 +1057,137 @@ func ExtractVisibility(addressable Addressable, actorFollowersURI string) (gtsmo  	return visibility, nil  } +// ExtractInteractionPolicy extracts a *gtsmodel.InteractionPolicy +// from the given Statusable created by by the given *gtsmodel.Account. +// +// 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. +func ExtractInteractionPolicy( +	statusable Statusable, +	owner *gtsmodel.Account, +) *gtsmodel.InteractionPolicy { +	policyProp := statusable.GetGoToSocialInteractionPolicy() +	if policyProp == nil || policyProp.Len() != 1 { +		return nil +	} + +	policyPropIter := policyProp.At(0) +	if !policyPropIter.IsGoToSocialInteractionPolicy() { +		return nil +	} + +	policy := policyPropIter.Get() +	if policy == nil { +		return nil +	} + +	return >smodel.InteractionPolicy{ +		CanLike:     extractCanLike(policy.GetGoToSocialCanLike(), owner), +		CanReply:    extractCanReply(policy.GetGoToSocialCanReply(), owner), +		CanAnnounce: extractCanAnnounce(policy.GetGoToSocialCanAnnounce(), owner), +	} +} + +func extractCanLike( +	prop vocab.GoToSocialCanLikeProperty, +	owner *gtsmodel.Account, +) gtsmodel.PolicyRules { +	if prop == nil || prop.Len() != 1 { +		return gtsmodel.PolicyRules{} +	} + +	propIter := prop.At(0) +	if !propIter.IsGoToSocialCanLike() { +		return gtsmodel.PolicyRules{} +	} + +	withRules := propIter.Get() +	if withRules == nil { +		return gtsmodel.PolicyRules{} +	} + +	return gtsmodel.PolicyRules{ +		Always:       extractPolicyValues(withRules.GetGoToSocialAlways(), owner), +		WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), 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{} +	} + +	withRules := propIter.Get() +	if withRules == nil { +		return gtsmodel.PolicyRules{} +	} + +	return gtsmodel.PolicyRules{ +		Always:       extractPolicyValues(withRules.GetGoToSocialAlways(), owner), +		WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), 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{} +	} + +	withRules := propIter.Get() +	if withRules == nil { +		return gtsmodel.PolicyRules{} +	} + +	return gtsmodel.PolicyRules{ +		Always:       extractPolicyValues(withRules.GetGoToSocialAlways(), owner), +		WithApproval: extractPolicyValues(withRules.GetGoToSocialApprovalRequired(), owner), +	} +} + +func extractPolicyValues[T WithIRI]( +	prop Property[T], +	owner *gtsmodel.Account, +) gtsmodel.PolicyValues { +	iris := getIRIs(prop) +	PolicyValues := make(gtsmodel.PolicyValues, 0, len(iris)) + +	for _, iri := range iris { +		switch iriStr := iri.String(); iriStr { +		case pub.PublicActivityPubIRI: +			PolicyValues = append(PolicyValues, gtsmodel.PolicyValuePublic) +		case owner.FollowersURI: +			PolicyValues = append(PolicyValues, gtsmodel.PolicyValueFollowers) +		case owner.FollowingURI: +			PolicyValues = append(PolicyValues, gtsmodel.PolicyValueFollowers) +		case owner.URI: +			PolicyValues = append(PolicyValues, gtsmodel.PolicyValueAuthor) +		default: +			if iri.Scheme == "http" || iri.Scheme == "https" { +				PolicyValues = append(PolicyValues, gtsmodel.PolicyValue(iriStr)) +			} +		} +	} + +	return PolicyValues +} +  // ExtractSensitive extracts whether or not an item should  // be marked as sensitive according to its ActivityStreams  // sensitive property. diff --git a/internal/ap/extractpolicy_test.go b/internal/ap/extractpolicy_test.go new file mode 100644 index 000000000..3d5e75c41 --- /dev/null +++ b/internal/ap/extractpolicy_test.go @@ -0,0 +1,137 @@ +// 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 ap_test + +import ( +	"bytes" +	"context" +	"io" +	"testing" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +type ExtractPolicyTestSuite struct { +	APTestSuite +} + +func (suite *ExtractPolicyTestSuite) TestExtractPolicy() { +	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": { +    "canLike": { +      "always": [ +        "https://www.w3.org/ns/activitystreams#Public" +      ], +      "approvalRequired": [] +    }, +    "canReply": { +      "always": [ +        "http://localhost:8080/users/the_mighty_zork", +        "http://localhost:8080/users/the_mighty_zork/followers", +        "https://gts.superseriousbusiness.org/users/dumpsterqueer", +        "https://gts.superseriousbusiness.org/users/f0x" +      ], +      "approvalRequired": [ +        "https://www.w3.org/ns/activitystreams#Public" +      ] +    }, +    "canAnnounce": { +      "always": [ +        "http://localhost:8080/users/the_mighty_zork" +      ], +      "approvalRequired": [ +        "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( +		context.Background(), +		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: gtsmodel.PolicyRules{ +			Always: gtsmodel.PolicyValues{ +				gtsmodel.PolicyValuePublic, +			}, +			WithApproval: gtsmodel.PolicyValues{}, +		}, +		CanReply: gtsmodel.PolicyRules{ +			Always: gtsmodel.PolicyValues{ +				gtsmodel.PolicyValueAuthor, +				gtsmodel.PolicyValueFollowers, +				"https://gts.superseriousbusiness.org/users/dumpsterqueer", +				"https://gts.superseriousbusiness.org/users/f0x", +			}, +			WithApproval: gtsmodel.PolicyValues{ +				gtsmodel.PolicyValuePublic, +			}, +		}, +		CanAnnounce: gtsmodel.PolicyRules{ +			Always: gtsmodel.PolicyValues{ +				gtsmodel.PolicyValueAuthor, +			}, +			WithApproval: gtsmodel.PolicyValues{ +				gtsmodel.PolicyValuePublic, +			}, +		}, +	} +	suite.EqualValues(expectedPolicy, policy) +} + +func TestExtractPolicyTestSuite(t *testing.T) { +	suite.Run(t, &ExtractPolicyTestSuite{}) +} diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 8f2e17c09..a721fa997 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -124,6 +124,24 @@ func ToPollOptionable(t vocab.Type) (PollOptionable, bool) {  	return note, true  } +// IsAccept returns whether AS vocab type name +// is something that can be cast to Accept. +func IsAcceptable(typeName string) bool { +	return typeName == ActivityAccept +} + +// ToAcceptable safely tries to cast vocab.Type as vocab.ActivityStreamsAccept. +// +// TODO: Add additional "Accept" types here, eg., "ApproveReply" from +// https://codeberg.org/fediverse/fep/src/branch/main/fep/5624/fep-5624.md +func ToAcceptable(t vocab.Type) (vocab.ActivityStreamsAccept, bool) { +	acceptable, ok := t.(vocab.ActivityStreamsAccept) +	if !ok || !IsAcceptable(t.GetTypeName()) { +		return nil, false +	} +	return acceptable, true +} +  // Activityable represents the minimum activitypub interface for representing an 'activity'.  // (see: IsActivityable() for types implementing this, though you MUST make sure to check  // the typeName as this bare interface may be implementable by non-Activityable types). @@ -188,6 +206,8 @@ type Statusable interface {  	WithAttachment  	WithTag  	WithReplies +	WithInteractionPolicy +	WithApprovedBy  }  // Pollable represents the minimum activitypub interface for representing a 'poll' (it's a subset of a status). @@ -217,6 +237,12 @@ type PollOptionable interface {  	WithAttributedTo  } +// Acceptable represents the minimum activitypub +// interface for representing an Accept. +type Acceptable interface { +	Activityable +} +  // Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. (see: IsAttachmentable).  // This interface is fulfilled by: Audio, Document, Image, Video  type Attachmentable interface { @@ -657,3 +683,21 @@ type WithVotersCount interface {  	GetTootVotersCount() vocab.TootVotersCountProperty  	SetTootVotersCount(vocab.TootVotersCountProperty)  } + +// WithReplies represents an object with GoToSocialInteractionPolicy. +type WithInteractionPolicy interface { +	GetGoToSocialInteractionPolicy() vocab.GoToSocialInteractionPolicyProperty +	SetGoToSocialInteractionPolicy(vocab.GoToSocialInteractionPolicyProperty) +} + +// WithPolicyRules represents an activity with always and approvalRequired properties. +type WithPolicyRules interface { +	GetGoToSocialAlways() vocab.GoToSocialAlwaysProperty +	GetGoToSocialApprovalRequired() vocab.GoToSocialApprovalRequiredProperty +} + +// WithApprovedBy represents a Statusable with the approvedBy property. +type WithApprovedBy interface { +	GetGoToSocialApprovedBy() vocab.GoToSocialApprovedByProperty +	SetGoToSocialApprovedBy(vocab.GoToSocialApprovedByProperty) +} diff --git a/internal/ap/normalize.go b/internal/ap/normalize.go index bef6d93b0..30d8515a5 100644 --- a/internal/ap/normalize.go +++ b/internal/ap/normalize.go @@ -575,6 +575,107 @@ func NormalizeOutgoingContentProp(item WithContent, rawJSON map[string]interface  	}  } +// NormalizeOutgoingInteractionPolicyProp replaces single-entry interactionPolicy values +// with single-entry arrays, for better compatibility with other AP implementations. +// +// Ie: +// +//	"interactionPolicy": { +//		"canAnnounce": { +//			"always": "https://www.w3.org/ns/activitystreams#Public", +//			"approvalRequired": [] +//		}, +//		"canLike": { +//			"always": "https://www.w3.org/ns/activitystreams#Public", +//			"approvalRequired": [] +//		}, +//		"canReply": { +//			"always": "https://www.w3.org/ns/activitystreams#Public", +//			"approvalRequired": [] +//		} +//	} +// +// becomes: +// +//	"interactionPolicy": { +//		"canAnnounce": { +//			"always": [ +//				"https://www.w3.org/ns/activitystreams#Public" +//			], +//			"approvalRequired": [] +//		}, +//		"canLike": { +//			"always": [ +//				"https://www.w3.org/ns/activitystreams#Public" +//			], +//			"approvalRequired": [] +//		}, +//		"canReply": { +//			"always": [ +//				"https://www.w3.org/ns/activitystreams#Public" +//			], +//			"approvalRequired": [] +//		} +//	} +// +// Noop for items with no attachments, or with attachments that are already a slice. +func NormalizeOutgoingInteractionPolicyProp(item WithInteractionPolicy, rawJSON map[string]interface{}) { +	policy, ok := rawJSON["interactionPolicy"] +	if !ok { +		// No 'interactionPolicy', +		// nothing to change. +		return +	} + +	policyMap, ok := policy.(map[string]interface{}) +	if !ok { +		// Malformed 'interactionPolicy', +		// nothing to change. +		return +	} + +	for _, rulesKey := range []string{ +		"canLike", +		"canReply", +		"canAnnounce", +	} { +		// Either "canAnnounce", +		// "canLike", or "canApprove" +		rulesVal, ok := policyMap[rulesKey] +		if !ok { +			// Not set. +			return +		} + +		rulesValMap, ok := rulesVal.(map[string]interface{}) +		if !ok { +			// Malformed or not +			// present skip. +			return +		} + +		for _, PolicyValuesKey := range []string{ +			"always", +			"approvalRequired", +		} { +			PolicyValuesVal, ok := rulesValMap[PolicyValuesKey] +			if !ok { +				// Not set. +				continue +			} + +			if _, ok := PolicyValuesVal.([]interface{}); ok { +				// Already slice, +				// nothing to change. +				continue +			} + +			// Coerce single-object to slice. +			rulesValMap[PolicyValuesKey] = []interface{}{PolicyValuesVal} +		} +	} +} +  // NormalizeOutgoingObjectProp normalizes each Object entry in the rawJSON of the given  // item by calling custom serialization / normalization functions on them in turn.  // diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 1bd8c303e..38e58ebc0 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -520,6 +520,27 @@ func SetManuallyApprovesFollowers(with WithManuallyApprovesFollowers, manuallyAp  	mafProp.Set(manuallyApprovesFollowers)  } +// GetApprovedBy returns the URL contained in +// the ApprovedBy property of 'with', if set. +func GetApprovedBy(with WithApprovedBy) *url.URL { +	mafProp := with.GetGoToSocialApprovedBy() +	if mafProp == nil || !mafProp.IsIRI() { +		return nil +	} +	return mafProp.Get() +} + +// SetApprovedBy sets the given url +// on the ApprovedBy property of 'with'. +func SetApprovedBy(with WithApprovedBy, approvedBy *url.URL) { +	abProp := with.GetGoToSocialApprovedBy() +	if abProp == nil { +		abProp = streams.NewGoToSocialApprovedByProperty() +		with.SetGoToSocialApprovedBy(abProp) +	} +	abProp.Set(approvedBy) +} +  // extractIRIs extracts just the AP IRIs from an iterable  // property that may contain types (with IRIs) or just IRIs.  // diff --git a/internal/ap/resolve.go b/internal/ap/resolve.go index b2e866b6f..76a8809c3 100644 --- a/internal/ap/resolve.go +++ b/internal/ap/resolve.go @@ -37,6 +37,8 @@ func ResolveIncomingActivity(r *http.Request) (pub.Activity, bool, gtserror.With  	// Get "raw" map  	// destination.  	raw := getMap() +	// Release. +	defer putMap(raw)  	// Decode data as JSON into 'raw' map  	// and get the resolved AS vocab.Type. @@ -79,9 +81,6 @@ func ResolveIncomingActivity(r *http.Request) (pub.Activity, bool, gtserror.With  	// (see: https://github.com/superseriousbusiness/gotosocial/issues/1661)  	NormalizeIncomingActivity(activity, raw) -	// Release. -	putMap(raw) -  	return activity, true, nil  } @@ -93,6 +92,8 @@ func ResolveStatusable(ctx context.Context, body io.ReadCloser) (Statusable, err  	// Get "raw" map  	// destination.  	raw := getMap() +	// Release. +	defer putMap(raw)  	// Decode data as JSON into 'raw' map  	// and get the resolved AS vocab.Type. @@ -121,9 +122,6 @@ func ResolveStatusable(ctx context.Context, body io.ReadCloser) (Statusable, err  	NormalizeIncomingSummary(statusable, raw)  	NormalizeIncomingName(statusable, raw) -	// Release. -	putMap(raw) -  	return statusable, nil  } @@ -135,6 +133,8 @@ func ResolveAccountable(ctx context.Context, body io.ReadCloser) (Accountable, e  	// Get "raw" map  	// destination.  	raw := getMap() +	// Release. +	defer putMap(raw)  	// Decode data as JSON into 'raw' map  	// and get the resolved AS vocab.Type. @@ -153,9 +153,6 @@ func ResolveAccountable(ctx context.Context, body io.ReadCloser) (Accountable, e  	NormalizeIncomingSummary(accountable, raw) -	// Release. -	putMap(raw) -  	return accountable, nil  } @@ -165,6 +162,8 @@ func ResolveCollection(ctx context.Context, body io.ReadCloser) (CollectionItera  	// Get "raw" map  	// destination.  	raw := getMap() +	// Release. +	defer putMap(raw)  	// Decode data as JSON into 'raw' map  	// and get the resolved AS vocab.Type. @@ -174,9 +173,6 @@ func ResolveCollection(ctx context.Context, body io.ReadCloser) (CollectionItera  		return nil, gtserror.SetWrongType(err)  	} -	// Release. -	putMap(raw) -  	// Cast as as Collection-like.  	return ToCollectionIterator(t)  } @@ -187,6 +183,8 @@ func ResolveCollectionPage(ctx context.Context, body io.ReadCloser) (CollectionP  	// Get "raw" map  	// destination.  	raw := getMap() +	// Release. +	defer putMap(raw)  	// Decode data as JSON into 'raw' map  	// and get the resolved AS vocab.Type. @@ -196,13 +194,40 @@ func ResolveCollectionPage(ctx context.Context, body io.ReadCloser) (CollectionP  		return nil, gtserror.SetWrongType(err)  	} -	// Release. -	putMap(raw) -  	// Cast as as CollectionPage-like.  	return ToCollectionPageIterator(t)  } +// ResolveAcceptable tries to resolve the given reader +// into an ActivityStreams Acceptable representation. +func ResolveAcceptable( +	ctx context.Context, +	body io.ReadCloser, +) (Acceptable, error) { +	// Get "raw" map +	// destination. +	raw := getMap() +	// Release. +	defer putMap(raw) + +	// Decode data as JSON into 'raw' map +	// and get the resolved AS vocab.Type. +	// (this handles close of given body). +	t, err := decodeType(ctx, body, raw) +	if err != nil { +		return nil, gtserror.SetWrongType(err) +	} + +	// Attempt to cast as acceptable. +	acceptable, ok := ToAcceptable(t) +	if !ok { +		err := gtserror.Newf("cannot resolve vocab type %T as acceptable", t) +		return nil, gtserror.SetWrongType(err) +	} + +	return acceptable, nil +} +  // emptydest is an empty JSON decode  // destination useful for "noop" decodes  // to check underlying reader is empty. diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go index b13ebb340..3e5a92d79 100644 --- a/internal/ap/serialize.go +++ b/internal/ap/serialize.go @@ -37,7 +37,7 @@ import (  //   - OrderedCollection:       'orderedItems' property will always be made into an array.  //   - OrderedCollectionPage:   'orderedItems' property will always be made into an array.  //   - Any Accountable type:    'attachment' property will always be made into an array. -//   - Any Statusable type:     'attachment' property will always be made into an array; 'content' and 'contentMap' will be normalized. +//   - Any Statusable type:     'attachment' property will always be made into an array; 'content', 'contentMap', and 'interactionPolicy' will be normalized.  //   - Any Activityable type:   any 'object's set on an activity will be custom serialized as above.  func Serialize(t vocab.Type) (m map[string]interface{}, e error) {  	switch tn := t.GetTypeName(); { @@ -153,6 +153,7 @@ func serializeStatusable(t vocab.Type, includeContext bool) (map[string]interfac  	NormalizeOutgoingAttachmentProp(statusable, data)  	NormalizeOutgoingContentProp(statusable, data) +	NormalizeOutgoingInteractionPolicyProp(statusable, data)  	return data, nil  } | 
