summaryrefslogtreecommitdiff
path: root/internal/ap
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-07-26 12:04:28 +0200
committerLibravatar GitHub <noreply@github.com>2024-07-26 12:04:28 +0200
commit8ab2b19a946251f258446d22f420d401f61d22f6 (patch)
tree39fb674f135fd1cfcf4de5b319913f0d0c17d11a /internal/ap
parent[docs] Add separate migration section + instructions for moving to GtS and no... (diff)
downloadgotosocial-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.go63
-rw-r--r--internal/ap/extract.go131
-rw-r--r--internal/ap/extractpolicy_test.go137
-rw-r--r--internal/ap/interfaces.go44
-rw-r--r--internal/ap/normalize.go101
-rw-r--r--internal/ap/properties.go21
-rw-r--r--internal/ap/resolve.go55
-rw-r--r--internal/ap/serialize.go3
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 &gtsmodel.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 := &gtsmodel.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
}