diff options
author | 2024-07-26 12:04:28 +0200 | |
---|---|---|
committer | 2024-07-26 12:04:28 +0200 | |
commit | 8ab2b19a946251f258446d22f420d401f61d22f6 (patch) | |
tree | 39fb674f135fd1cfcf4de5b319913f0d0c17d11a /internal/ap | |
parent | [docs] Add separate migration section + instructions for moving to GtS and no... (diff) | |
download | gotosocial-8ab2b19a946251f258446d22f420d401f61d22f6.tar.xz |
[feature] Federate interaction policies + Accepts; enforce policies (#3138)
* [feature] Federate interaction policies + Accepts; enforce policies
* use Acceptable type
* fix index
* remove appendIRIStrs
* add GetAccept federatingdb function
* lock on object IRI
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 } |