mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 07:12:25 -05:00
[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
This commit is contained in:
parent
f8d399cf6a
commit
8ab2b19a94
42 changed files with 3541 additions and 254 deletions
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
137
internal/ap/extractpolicy_test.go
Normal file
137
internal/ap/extractpolicy_test.go
Normal file
|
|
@ -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{})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
55
internal/api/activitypub/users/acceptget.go
Normal file
55
internal/api/activitypub/users/acceptget.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// 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 users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// AcceptGETHandler serves an interactionApproval as an ActivityStreams Accept.
|
||||
func (m *Module) AcceptGETHandler(c *gin.Context) {
|
||||
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
acceptID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
contentType, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.Fedi().AcceptGet(c.Request.Context(), username, acceptID)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSONType(c, http.StatusOK, contentType, resp)
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
)
|
||||
|
|
@ -55,6 +56,8 @@ const (
|
|||
StatusPath = BasePath + "/" + uris.StatusesPath + "/:" + StatusIDKey
|
||||
// StatusRepliesPath is for serving the replies collection of a status.
|
||||
StatusRepliesPath = StatusPath + "/replies"
|
||||
// AcceptPath is for serving accepts of a status.
|
||||
AcceptPath = BasePath + "/" + uris.AcceptsPath + "/:" + apiutil.IDKey
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
|
|
@ -76,4 +79,5 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
|||
attachHandler(http.MethodGet, StatusPath, m.StatusGETHandler)
|
||||
attachHandler(http.MethodGet, StatusRepliesPath, m.StatusRepliesGETHandler)
|
||||
attachHandler(http.MethodGet, OutboxPath, m.OutboxGETHandler)
|
||||
attachHandler(http.MethodGet, AcceptPath, m.AcceptGETHandler)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
// 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 migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
up := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
if _, err := tx.
|
||||
NewCreateTable().
|
||||
Model(>smodel.InteractionApproval{}).
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("interaction_approvals").
|
||||
Index("interaction_approvals_account_id_idx").
|
||||
Column("account_id").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.
|
||||
NewCreateIndex().
|
||||
Table("interaction_approvals").
|
||||
Index("interaction_approvals_interacting_account_id_idx").
|
||||
Column("interacting_account_id").
|
||||
IfNotExists().
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
down := func(ctx context.Context, db *bun.DB) error {
|
||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := Migrations.Register(up, down); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -19,10 +19,13 @@ package dereferencing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// isPermittedStatus returns whether the given status
|
||||
|
|
@ -147,12 +150,67 @@ func (d *Dereferencer) isPermittedReply(
|
|||
return onFalse()
|
||||
}
|
||||
|
||||
// TODO in next PR: check conditional /
|
||||
// with approval and deref Accept.
|
||||
if !replyable.Permitted() {
|
||||
if replyable.Permitted() &&
|
||||
!replyable.MatchedOnCollection() {
|
||||
// Replier is permitted to do this
|
||||
// interaction, and didn't match on
|
||||
// a collection so we don't need to
|
||||
// do further checking.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Replier is permitted to do this
|
||||
// interaction pending approval, or
|
||||
// permitted but matched on a collection.
|
||||
//
|
||||
// Check if we can dereference
|
||||
// an Accept that grants approval.
|
||||
|
||||
if status.ApprovedByURI == "" {
|
||||
// Status doesn't claim to be approved.
|
||||
//
|
||||
// For replies to local statuses that's
|
||||
// fine, we can put it in the DB pending
|
||||
// approval, and continue processing it.
|
||||
//
|
||||
// If permission was granted based on a match
|
||||
// with a followers or following collection,
|
||||
// we can mark it as PreApproved so the processor
|
||||
// sends an accept out for it immediately.
|
||||
//
|
||||
// For replies to remote statuses, though
|
||||
// we should be polite and just drop it.
|
||||
if inReplyTo.IsLocal() {
|
||||
status.PendingApproval = util.Ptr(true)
|
||||
status.PreApproved = replyable.MatchedOnCollection()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Status claims to be approved, check
|
||||
// this by dereferencing the Accept and
|
||||
// inspecting the return value.
|
||||
if err := d.validateApprovedBy(
|
||||
ctx,
|
||||
requestUser,
|
||||
status.ApprovedByURI,
|
||||
status.URI,
|
||||
inReplyTo.AccountURI,
|
||||
); err != nil {
|
||||
// Error dereferencing means we couldn't
|
||||
// get the Accept right now or it wasn't
|
||||
// valid, so we shouldn't store this status.
|
||||
//
|
||||
// Do log the error though as it may be
|
||||
// interesting for admins to see.
|
||||
log.Info(ctx, "rejecting reply with undereferenceable ApprovedByURI: %v", err)
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Status has been approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -206,11 +264,203 @@ func (d *Dereferencer) isPermittedBoost(
|
|||
return onFalse()
|
||||
}
|
||||
|
||||
// TODO in next PR: check conditional /
|
||||
// with approval and deref Accept.
|
||||
if !boostable.Permitted() {
|
||||
if boostable.Permitted() &&
|
||||
!boostable.MatchedOnCollection() {
|
||||
// Booster is permitted to do this
|
||||
// interaction, and didn't match on
|
||||
// a collection so we don't need to
|
||||
// do further checking.
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Booster is permitted to do this
|
||||
// interaction pending approval, or
|
||||
// permitted but matched on a collection.
|
||||
//
|
||||
// Check if we can dereference
|
||||
// an Accept that grants approval.
|
||||
|
||||
if status.ApprovedByURI == "" {
|
||||
// Status doesn't claim to be approved.
|
||||
//
|
||||
// For boosts of local statuses that's
|
||||
// fine, we can put it in the DB pending
|
||||
// approval, and continue processing it.
|
||||
//
|
||||
// If permission was granted based on a match
|
||||
// with a followers or following collection,
|
||||
// we can mark it as PreApproved so the processor
|
||||
// sends an accept out for it immediately.
|
||||
//
|
||||
// For boosts of remote statuses, though
|
||||
// we should be polite and just drop it.
|
||||
if boostOf.IsLocal() {
|
||||
status.PendingApproval = util.Ptr(true)
|
||||
status.PreApproved = boostable.MatchedOnCollection()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Boost claims to be approved, check
|
||||
// this by dereferencing the Accept and
|
||||
// inspecting the return value.
|
||||
if err := d.validateApprovedBy(
|
||||
ctx,
|
||||
requestUser,
|
||||
status.ApprovedByURI,
|
||||
status.URI,
|
||||
boostOf.AccountURI,
|
||||
); err != nil {
|
||||
// Error dereferencing means we couldn't
|
||||
// get the Accept right now or it wasn't
|
||||
// valid, so we shouldn't store this status.
|
||||
//
|
||||
// Do log the error though as it may be
|
||||
// interesting for admins to see.
|
||||
log.Info(ctx, "rejecting boost with undereferenceable ApprovedByURI: %v", err)
|
||||
return onFalse()
|
||||
}
|
||||
|
||||
// Status has been approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// validateApprovedBy dereferences the activitystreams Accept at
|
||||
// the specified IRI, and checks the Accept for validity against
|
||||
// the provided expectedObject and expectedActor.
|
||||
//
|
||||
// Will return either nil if everything looked OK, or an error if
|
||||
// something went wrong during deref, or if the dereffed Accept
|
||||
// did not meet expectations.
|
||||
func (d *Dereferencer) validateApprovedBy(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
approvedByURIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03"
|
||||
expectedObject string, // Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
|
||||
expectedActor string, // Eg., "https://example.org/users/someone"
|
||||
) error {
|
||||
approvedByURI, err := url.Parse(approvedByURIStr)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error parsing approvedByURI: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Don't make calls to the remote if it's blocked.
|
||||
if blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByURI.Host); blocked || err != nil {
|
||||
err := gtserror.Newf("domain %s is blocked", approvedByURI.Host)
|
||||
return err
|
||||
}
|
||||
|
||||
transport, err := d.transportController.NewTransportForUsername(ctx, requestUser)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error creating transport: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Make the call to resolve into an Acceptable.
|
||||
rsp, err := transport.Dereference(ctx, approvedByURI)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error dereferencing %s: %w", approvedByURIStr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
acceptable, err := ap.ResolveAcceptable(ctx, rsp.Body)
|
||||
|
||||
// Tidy up rsp body.
|
||||
_ = rsp.Body.Close()
|
||||
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error resolving Accept %s: %w", approvedByURIStr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract the URI/ID of the Accept.
|
||||
acceptURI := ap.GetJSONLDId(acceptable)
|
||||
acceptURIStr := acceptURI.String()
|
||||
|
||||
// Check whether input URI and final returned URI
|
||||
// have changed (i.e. we followed some redirects).
|
||||
rspURL := rsp.Request.URL
|
||||
rspURLStr := rspURL.String()
|
||||
if rspURLStr != approvedByURIStr {
|
||||
// Final URI was different from approvedByURIStr.
|
||||
//
|
||||
// Make sure it's at least on the same host as
|
||||
// what we expected (ie., we weren't redirected
|
||||
// across domains), and make sure it's the same
|
||||
// as the ID of the Accept we were returned.
|
||||
if rspURL.Host != approvedByURI.Host {
|
||||
err := gtserror.Newf(
|
||||
"final dereference host %s did not match approvedByURI host %s",
|
||||
rspURL.Host, approvedByURI.Host,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if acceptURIStr != rspURLStr {
|
||||
err := gtserror.Newf(
|
||||
"final dereference uri %s did not match returned Accept ID/URI %s",
|
||||
rspURLStr, acceptURIStr,
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the Accept URI has the same host
|
||||
// as the Accept Actor, so we know we're
|
||||
// not dealing with someone on a different
|
||||
// domain just pretending to be the Actor.
|
||||
actorIRIs := ap.GetActorIRIs(acceptable)
|
||||
if len(actorIRIs) != 1 {
|
||||
err := gtserror.New("resolved Accept actor(s) length was not 1")
|
||||
return gtserror.SetMalformed(err)
|
||||
}
|
||||
|
||||
actorIRI := actorIRIs[0]
|
||||
actorStr := actorIRI.String()
|
||||
|
||||
if actorIRI.Host != acceptURI.Host {
|
||||
err := gtserror.Newf(
|
||||
"Accept Actor %s was not the same host as Accept %s",
|
||||
actorStr, acceptURIStr,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the Accept Actor is who we expect
|
||||
// it to be, and not someone else trying to
|
||||
// do an Accept for an interaction with a
|
||||
// statusable they don't own.
|
||||
if actorStr != expectedActor {
|
||||
err := gtserror.Newf(
|
||||
"Accept Actor %s was not the same as expected actor %s",
|
||||
actorStr, expectedActor,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure the Accept Object is what we expect
|
||||
// it to be, ie., it's Accepting the interaction
|
||||
// we need it to Accept, and not something else.
|
||||
objectIRIs := ap.GetObjectIRIs(acceptable)
|
||||
if len(objectIRIs) != 1 {
|
||||
err := gtserror.New("resolved Accept object(s) length was not 1")
|
||||
return err
|
||||
}
|
||||
|
||||
objectIRI := objectIRIs[0]
|
||||
objectStr := objectIRI.String()
|
||||
|
||||
if objectStr != expectedObject {
|
||||
err := gtserror.Newf(
|
||||
"resolved Accept Object uri %s was not the same as expected object %s",
|
||||
objectStr, expectedObject,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,16 +20,31 @@ package federatingdb
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"codeberg.org/gruf/go-logger/v2/level"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (f *federatingDB) GetAccept(
|
||||
ctx context.Context,
|
||||
acceptIRI *url.URL,
|
||||
) (vocab.ActivityStreamsAccept, error) {
|
||||
approval, err := f.state.DB.GetInteractionApprovalByURI(ctx, acceptIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.converter.InteractionApprovalToASAccept(ctx, approval)
|
||||
}
|
||||
|
||||
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
|
||||
if log.Level() >= level.DEBUG {
|
||||
i, err := marshalItem(accept)
|
||||
|
|
@ -55,100 +70,382 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
|||
return nil
|
||||
}
|
||||
|
||||
// Iterate all provided objects in the activity.
|
||||
activityID := ap.GetJSONLDId(accept)
|
||||
if activityID == nil {
|
||||
// We need an ID.
|
||||
const text = "Accept had no id property"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Iterate all provided objects in the activity,
|
||||
// handling the ones we know how to handle.
|
||||
for _, object := range ap.ExtractObjects(accept) {
|
||||
if asType := object.GetType(); asType != nil {
|
||||
// Check and handle any
|
||||
// vocab.Type objects.
|
||||
// nolint:gocritic
|
||||
switch asType.GetTypeName() {
|
||||
|
||||
// Check and handle any vocab.Type objects.
|
||||
if objType := object.GetType(); objType != nil {
|
||||
switch objType.GetTypeName() { //nolint:gocritic
|
||||
|
||||
// ACCEPT FOLLOW
|
||||
case ap.ActivityFollow:
|
||||
// Cast the vocab.Type object to known AS type.
|
||||
asFollow := objType.(vocab.ActivityStreamsFollow)
|
||||
|
||||
// convert the follow to something we can understand
|
||||
gtsFollow, err := f.converter.ASFollowToFollow(ctx, asFollow)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
|
||||
if err := f.acceptFollowType(
|
||||
ctx,
|
||||
asType,
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the creator of the original follow
|
||||
// is the same as whatever inbox this landed in.
|
||||
if gtsFollow.AccountID != receivingAcct.ID {
|
||||
return errors.New("ACCEPT: follow account and inbox account were not the same")
|
||||
}
|
||||
} else if object.IsIRI() {
|
||||
// Check and handle any
|
||||
// IRI type objects.
|
||||
switch objIRI := object.GetIRI(); {
|
||||
|
||||
// Make sure the target of the original follow
|
||||
// is the same as the account making the request.
|
||||
if gtsFollow.TargetAccountID != requestingAcct.ID {
|
||||
return errors.New("ACCEPT: follow target account and requesting account were not the same")
|
||||
}
|
||||
|
||||
follow, err := f.state.DB.AcceptFollowRequest(ctx, gtsFollow.AccountID, gtsFollow.TargetAccountID)
|
||||
if err != nil {
|
||||
// ACCEPT FOLLOW
|
||||
case uris.IsFollowPath(objIRI):
|
||||
if err := f.acceptFollowIRI(
|
||||
ctx,
|
||||
objIRI.String(),
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
}
|
||||
// ACCEPT STATUS (reply/boost)
|
||||
case uris.IsStatusesPath(objIRI):
|
||||
if err := f.acceptStatusIRI(
|
||||
ctx,
|
||||
activityID.String(),
|
||||
objIRI.String(),
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
// ACCEPT LIKE
|
||||
case uris.IsLikePath(objIRI):
|
||||
if err := f.acceptLikeIRI(
|
||||
ctx,
|
||||
activityID.String(),
|
||||
objIRI.String(),
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check and handle any
|
||||
// IRI type objects.
|
||||
if object.IsIRI() {
|
||||
|
||||
// Extract IRI from object.
|
||||
iri := object.GetIRI()
|
||||
if !uris.IsFollowPath(iri) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Serialize IRI.
|
||||
iriStr := iri.String()
|
||||
|
||||
// ACCEPT FOLLOW
|
||||
followReq, err := f.state.DB.GetFollowRequestByURI(ctx, iriStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", iriStr, err)
|
||||
}
|
||||
|
||||
// Make sure the creator of the original follow
|
||||
// is the same as whatever inbox this landed in.
|
||||
if followReq.AccountID != receivingAcct.ID {
|
||||
return errors.New("ACCEPT: follow account and inbox account were not the same")
|
||||
}
|
||||
|
||||
// Make sure the target of the original follow
|
||||
// is the same as the account making the request.
|
||||
if followReq.TargetAccountID != requestingAcct.ID {
|
||||
return errors.New("ACCEPT: follow target account and requesting account were not the same")
|
||||
}
|
||||
|
||||
follow, err := f.state.DB.AcceptFollowRequest(ctx, followReq.AccountID, followReq.TargetAccountID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptFollowType(
|
||||
ctx context.Context,
|
||||
asType vocab.Type,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Cast the vocab.Type object to known AS type.
|
||||
asFollow := asType.(vocab.ActivityStreamsFollow)
|
||||
|
||||
// Reconstruct the follow.
|
||||
follow, err := f.converter.ASFollowToFollow(ctx, asFollow)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting Follow to *gtsmodel.Follow: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Lock on the Follow URI
|
||||
// as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(follow.URI)
|
||||
defer unlock()
|
||||
|
||||
// Make sure the creator of the original follow
|
||||
// is the same as whatever inbox this landed in.
|
||||
if follow.AccountID != receivingAcct.ID {
|
||||
const text = "Follow account and inbox account were not the same"
|
||||
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Make sure the target of the original follow
|
||||
// is the same as the account making the request.
|
||||
if follow.TargetAccountID != requestingAcct.ID {
|
||||
const text = "Follow target account and requesting account were not the same"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Accept and get the populated follow back.
|
||||
follow, err = f.state.DB.AcceptFollowRequest(
|
||||
ctx,
|
||||
follow.AccountID,
|
||||
follow.TargetAccountID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error accepting follow request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if follow == nil {
|
||||
// There was no follow request
|
||||
// to accept, just return 202.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send the accepted follow through
|
||||
// the processor to do side effects.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptFollowIRI(
|
||||
ctx context.Context,
|
||||
objectIRI string,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Lock on this potential Follow
|
||||
// URI as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(objectIRI)
|
||||
defer unlock()
|
||||
|
||||
// Get the follow req from the db.
|
||||
followReq, err := f.state.DB.GetFollowRequestByURI(ctx, objectIRI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting follow request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if followReq == nil {
|
||||
// We didn't have a follow request
|
||||
// with this URI, so nothing to do.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure the creator of the original follow
|
||||
// is the same as whatever inbox this landed in.
|
||||
if followReq.AccountID != receivingAcct.ID {
|
||||
const text = "Follow account and inbox account were not the same"
|
||||
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Make sure the target of the original follow
|
||||
// is the same as the account making the request.
|
||||
if followReq.TargetAccountID != requestingAcct.ID {
|
||||
const text = "Follow target account and requesting account were not the same"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Accept and get the populated follow back.
|
||||
follow, err := f.state.DB.AcceptFollowRequest(
|
||||
ctx,
|
||||
followReq.AccountID,
|
||||
followReq.TargetAccountID,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error accepting follow request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if follow == nil {
|
||||
// There was no follow request
|
||||
// to accept, just return 202.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Send the accepted follow through
|
||||
// the processor to do side effects.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityFollow,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: follow,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptStatusIRI(
|
||||
ctx context.Context,
|
||||
activityID string,
|
||||
objectIRI string,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Lock on this potential status
|
||||
// URI as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(objectIRI)
|
||||
defer unlock()
|
||||
|
||||
// Get the status from the db.
|
||||
status, err := f.state.DB.GetStatusByURI(ctx, objectIRI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting status: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if status == nil {
|
||||
// We didn't have a status with
|
||||
// this URI, so nothing to do.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !status.IsLocal() {
|
||||
// We don't process Accepts of statuses
|
||||
// that weren't created on our instance.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
if util.PtrOrValue(status.PendingApproval, false) {
|
||||
// Status doesn't need approval or it's
|
||||
// already been approved by an Accept.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure the creator of the original status
|
||||
// is the same as the inbox processing the Accept;
|
||||
// this also ensures the status is local.
|
||||
if status.AccountID != receivingAcct.ID {
|
||||
const text = "status author account and inbox account were not the same"
|
||||
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Make sure the target of the interaction (reply/boost)
|
||||
// is the same as the account doing the Accept.
|
||||
if status.BoostOfAccountID != requestingAcct.ID &&
|
||||
status.InReplyToAccountID != requestingAcct.ID {
|
||||
const text = "status reply to or boost of account and requesting account were not the same"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Mark the status as approved by this Accept URI.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.ApprovedByURI = activityID
|
||||
if err := f.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error accepting status: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
var apObjectType string
|
||||
if status.InReplyToID != "" {
|
||||
// Accepting a Reply.
|
||||
apObjectType = ap.ObjectNote
|
||||
} else {
|
||||
// Accepting an Announce.
|
||||
apObjectType = ap.ActivityAnnounce
|
||||
}
|
||||
|
||||
// Send the now-approved status through to the
|
||||
// fedi worker again to process side effects.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: apObjectType,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: status,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federatingDB) acceptLikeIRI(
|
||||
ctx context.Context,
|
||||
activityID string,
|
||||
objectIRI string,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Lock on this potential Like
|
||||
// URI as we may be updating it.
|
||||
unlock := f.state.FedLocks.Lock(objectIRI)
|
||||
defer unlock()
|
||||
|
||||
// Get the fave from the db.
|
||||
fave, err := f.state.DB.GetStatusFaveByURI(ctx, objectIRI)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting fave: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if fave == nil {
|
||||
// We didn't have a fave with
|
||||
// this URI, so nothing to do.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !fave.Account.IsLocal() {
|
||||
// We don't process Accepts of Likes
|
||||
// that weren't created on our instance.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !util.PtrOrValue(fave.PendingApproval, false) {
|
||||
// Like doesn't need approval or it's
|
||||
// already been approved by an Accept.
|
||||
// Just return.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure the creator of the original Like
|
||||
// is the same as the inbox processing the Accept;
|
||||
// this also ensures the Like is local.
|
||||
if fave.AccountID != receivingAcct.ID {
|
||||
const text = "fave creator account and inbox account were not the same"
|
||||
return gtserror.NewErrorUnprocessableEntity(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Make sure the target of the Like is the
|
||||
// same as the account doing the Accept.
|
||||
if fave.TargetAccountID != requestingAcct.ID {
|
||||
const text = "status fave target account and requesting account were not the same"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Mark the fave as approved by this Accept URI.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.ApprovedByURI = activityID
|
||||
if err := f.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error accepting status: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Send the now-approved fave through to the
|
||||
// fedi worker again to process side effects.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityLike,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: fave,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package federatingdb
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
|
|
@ -43,6 +44,12 @@ type DB interface {
|
|||
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
|
||||
Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error
|
||||
Move(ctx context.Context, move vocab.ActivityStreamsMove) error
|
||||
|
||||
/*
|
||||
Extra/convenience functionality.
|
||||
*/
|
||||
|
||||
GetAccept(ctx context.Context, acceptIRI *url.URL) (vocab.ActivityStreamsAccept, error)
|
||||
}
|
||||
|
||||
// FederatingDB uses the given state interface
|
||||
|
|
|
|||
|
|
@ -37,22 +37,30 @@ func (f *federatingDB) Get(ctx context.Context, id *url.URL) (value vocab.Type,
|
|||
l.Debug("entering Get")
|
||||
|
||||
switch {
|
||||
|
||||
case uris.IsUserPath(id):
|
||||
acct, err := f.state.DB.GetAccountByURI(ctx, id.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.converter.AccountToAS(ctx, acct)
|
||||
|
||||
case uris.IsStatusesPath(id):
|
||||
status, err := f.state.DB.GetStatusByURI(ctx, id.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.converter.StatusToAS(ctx, status)
|
||||
|
||||
case uris.IsFollowersPath(id):
|
||||
return f.Followers(ctx, id)
|
||||
|
||||
case uris.IsFollowingPath(id):
|
||||
return f.Following(ctx, id)
|
||||
|
||||
case uris.IsAcceptsPath(id):
|
||||
return f.GetAccept(ctx, id)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("federatingDB: could not Get %s", id.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ type Status struct {
|
|||
Federated *bool `bun:",notnull"` // This status will be federated beyond the local timeline(s)
|
||||
InteractionPolicy *InteractionPolicy `bun:""` // InteractionPolicy for this status. If null then the default InteractionPolicy should be assumed for this status's Visibility. Always null for boost wrappers.
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then status is a reply or boost wrapper that must be Approved by the reply-ee or boost-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then status is a reply to or boost wrapper of a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves the Announce or Create Activity that this status was/will be attached to.
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,5 +32,6 @@ type StatusFave struct {
|
|||
Status *Status `bun:"-"` // the faved status
|
||||
URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of this fave
|
||||
PendingApproval *bool `bun:",nullzero,notnull,default:false"` // If true then Like must be Approved by the like-ee before being fully distributed.
|
||||
PreApproved bool `bun:"-"` // If true, then fave targets a status on our instance, has permission to do the interaction, and an Accept should be sent out for it immediately. Field not stored in the DB.
|
||||
ApprovedByURI string `bun:",nullzero"` // URI of an Accept Activity that approves this Like.
|
||||
}
|
||||
|
|
|
|||
84
internal/processing/fedi/accept.go
Normal file
84
internal/processing/fedi/accept.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// 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 fedi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// AcceptGet handles the getting of a fedi/activitypub
|
||||
// representation of a local interaction approval.
|
||||
//
|
||||
// It performs appropriate authentication before
|
||||
// returning a JSON serializable interface.
|
||||
func (p *Processor) AcceptGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
approvalID string,
|
||||
) (interface{}, gtserror.WithCode) {
|
||||
// Authenticate incoming request, getting related accounts.
|
||||
auth, errWithCode := p.authenticate(ctx, requestedUser)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
if auth.handshakingURI != nil {
|
||||
// We're currently handshaking, which means
|
||||
// we don't know this account yet. This should
|
||||
// be a very rare race condition.
|
||||
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
receivingAcct := auth.receivingAcct
|
||||
|
||||
approval, err := p.state.DB.GetInteractionApprovalByID(ctx, approvalID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting approval %s: %w", approvalID, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if approval.AccountID != receivingAcct.ID {
|
||||
const text = "approval does not belong to receiving account"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
if approval == nil {
|
||||
err := gtserror.Newf("approval %s not found", approvalID)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
accept, err := p.converter.InteractionApprovalToASAccept(ctx, approval)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting approval: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
data, err := ap.Serialize(accept)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error serializing accept: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
|
@ -177,8 +177,7 @@ func NewProcessor(
|
|||
visFilter *visibility.Filter,
|
||||
intFilter *interaction.Filter,
|
||||
) *Processor {
|
||||
var parseMentionFunc = GetParseMentionFunc(state, federator)
|
||||
|
||||
parseMentionFunc := GetParseMentionFunc(state, federator)
|
||||
processor := &Processor{
|
||||
converter: converter,
|
||||
oauthServer: oauthServer,
|
||||
|
|
|
|||
|
|
@ -104,9 +104,18 @@ func (p *Processor) BoostCreate(
|
|||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *target.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
boost.PreApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
|
|
|
|||
|
|
@ -221,9 +221,18 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
|
|||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *inReplyTo.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
status.PreApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
|
|
|
|||
|
|
@ -103,8 +103,13 @@ func (p *Processor) FaveCreate(
|
|||
return nil, gtserror.NewErrorForbidden(err, errText)
|
||||
}
|
||||
|
||||
// Derive pendingApproval status.
|
||||
var pendingApproval bool
|
||||
// Derive pendingApproval
|
||||
// and preapproved status.
|
||||
var (
|
||||
pendingApproval bool
|
||||
preApproved bool
|
||||
)
|
||||
|
||||
switch {
|
||||
case policyResult.WithApproval():
|
||||
// We're allowed to do
|
||||
|
|
@ -115,9 +120,18 @@ func (p *Processor) FaveCreate(
|
|||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *status.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
preApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
|
|
@ -138,6 +152,7 @@ func (p *Processor) FaveCreate(
|
|||
StatusID: status.ID,
|
||||
Status: status,
|
||||
URI: uris.GenerateURIForLike(requester.Username, faveID),
|
||||
PreApproved: preApproved,
|
||||
PendingApproval: &pendingApproval,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,14 @@ import (
|
|||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// federate wraps functions for federating
|
||||
|
|
@ -135,6 +137,12 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
|
|||
return nil
|
||||
}
|
||||
|
||||
// CreateStatus sends the given status out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the status is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it replies to,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
// Do nothing if the status
|
||||
// shouldn't be federated.
|
||||
|
|
@ -153,18 +161,32 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
|
|||
return gtserror.Newf("error populating status: %w", err)
|
||||
}
|
||||
|
||||
// Parse the outbox URI of the status author.
|
||||
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert status to AS Statusable implementing type.
|
||||
statusable, err := f.converter.StatusToAS(ctx, status)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting status to Statusable: %w", err)
|
||||
}
|
||||
|
||||
// If status is pending approval,
|
||||
// it must be a reply. Deliver it
|
||||
// **ONLY** to the account it replies
|
||||
// to, on behalf of the replier.
|
||||
if util.PtrOrValue(status.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
status.Account,
|
||||
status.InReplyToAccount,
|
||||
// Status has to be wrapped in Create activity.
|
||||
typeutils.WrapStatusableInCreate(statusable, false),
|
||||
)
|
||||
}
|
||||
|
||||
// Parse the outbox URI of the status author.
|
||||
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a Create activity with Statusable via the Actor's outbox.
|
||||
create := typeutils.WrapStatusableInCreate(statusable, false)
|
||||
if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil {
|
||||
|
|
@ -672,6 +694,12 @@ func (f *federate) RejectFollow(ctx context.Context, follow *gtsmodel.Follow) er
|
|||
return nil
|
||||
}
|
||||
|
||||
// Like sends the given fave out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the fave is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it faves,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
|
|
@ -684,18 +712,30 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(fave.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the ActivityStreams Like.
|
||||
like, err := f.converter.FaveToAS(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting fave to AS Like: %w", err)
|
||||
}
|
||||
|
||||
// If fave is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it faves, on behalf of the faver.
|
||||
if util.PtrOrValue(fave.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
fave.Account,
|
||||
fave.TargetAccount,
|
||||
like,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(fave.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the Like via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, like,
|
||||
|
|
@ -709,6 +749,12 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Announce sends the given boost out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the boost is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it boosts,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatus(ctx, boost); err != nil {
|
||||
|
|
@ -721,12 +767,6 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(boost.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the ActivityStreams Announce.
|
||||
announce, err := f.converter.BoostToAS(
|
||||
ctx,
|
||||
|
|
@ -738,6 +778,24 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
|||
return gtserror.Newf("error converting boost to AS: %w", err)
|
||||
}
|
||||
|
||||
// If announce is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it boosts, on behalf of the booster.
|
||||
if util.PtrOrValue(boost.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
boost.Account,
|
||||
boost.BoostOfAccount,
|
||||
announce,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(boost.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the Announce via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, announce,
|
||||
|
|
@ -751,6 +809,57 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// deliverToInboxOnly delivers the given Activity
|
||||
// *only* to the inbox of targetAcct, on behalf of
|
||||
// sendingAcct, regardless of the `to` and `cc` values
|
||||
// set on the activity. This should be used specifically
|
||||
// for sending "pending approval" activities.
|
||||
func (f *federate) deliverToInboxOnly(
|
||||
ctx context.Context,
|
||||
sendingAcct *gtsmodel.Account,
|
||||
targetAcct *gtsmodel.Account,
|
||||
t vocab.Type,
|
||||
) error {
|
||||
if targetAcct.IsLocal() {
|
||||
// If this is a local target,
|
||||
// they've already received it.
|
||||
return nil
|
||||
}
|
||||
|
||||
toInbox, err := url.Parse(targetAcct.InboxURI)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error parsing target inbox uri: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
tsport, err := f.TransportController().NewTransportForUsername(
|
||||
ctx,
|
||||
sendingAcct.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error getting transport to deliver activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
m, err := ap.Serialize(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tsport.Deliver(ctx, m, toInbox); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error delivering activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateAccount(ctx, account); err != nil {
|
||||
|
|
@ -1015,3 +1124,75 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) AcceptInteraction(
|
||||
ctx context.Context,
|
||||
approval *gtsmodel.InteractionApproval,
|
||||
) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateInteractionApproval(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error populating approval: %w", err)
|
||||
}
|
||||
|
||||
// Bail if interacting account is ours:
|
||||
// we've already accepted internally and
|
||||
// shouldn't send an Accept to ourselves.
|
||||
if approval.InteractingAccount.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bail if account isn't ours:
|
||||
// we can't Accept on another
|
||||
// instance's behalf. (This
|
||||
// should never happen but...)
|
||||
if approval.Account.IsRemote() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(approval.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acceptingAcctIRI, err := parseURI(approval.Account.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interactingAcctURI, err := parseURI(approval.InteractingAccount.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interactionURI, err := parseURI(approval.InteractionURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Accept.
|
||||
accept := streams.NewActivityStreamsAccept()
|
||||
|
||||
// Set interacted-with account
|
||||
// as Actor of the Accept.
|
||||
ap.AppendActorIRIs(accept, acceptingAcctIRI)
|
||||
|
||||
// Set the interacted-with object
|
||||
// as Object of the Accept.
|
||||
ap.AppendObjectIRIs(accept, interactionURI)
|
||||
|
||||
// Address the Accept To the interacting acct.
|
||||
ap.AppendTo(accept, interactingAcctURI)
|
||||
|
||||
// Send the Accept via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, accept,
|
||||
); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error sending activity %T for %v via outbox %s: %w",
|
||||
accept, approval.InteractionType, outboxIRI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
|||
// ACCEPT USER (ie., new user+account sign-up)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.AcceptUser(ctx, cMsg)
|
||||
|
||||
// ACCEPT NOTE/STATUS (ie., accept a reply)
|
||||
case ap.ObjectNote:
|
||||
return p.clientAPI.AcceptReply(ctx, cMsg)
|
||||
|
||||
// ACCEPT LIKE
|
||||
case ap.ActivityLike:
|
||||
return p.clientAPI.AcceptLike(ctx, cMsg)
|
||||
|
||||
// ACCEPT BOOST
|
||||
case ap.ActivityAnnounce:
|
||||
return p.clientAPI.AcceptAnnounce(ctx, cMsg)
|
||||
}
|
||||
|
||||
// REJECT SOMETHING
|
||||
|
|
@ -236,6 +248,61 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
|
|||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// If pending approval is true then status must
|
||||
// reply to a status (either one of ours or a
|
||||
// remote) that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
status.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then send out the Create to
|
||||
// only the replied-to account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending reply.
|
||||
if err := p.surface.notifyPendingReply(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending reply: %v", err)
|
||||
}
|
||||
|
||||
// Send Create to *remote* account inbox ONLY.
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error federating pending reply: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Create with the approval
|
||||
// URI attached.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the status with approvedBy URI.
|
||||
approval, err := p.utils.approveReply(ctx, status)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving reply: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
|
|
@ -362,6 +429,61 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
|||
return gtserror.Newf("error populating status fave: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then fave must
|
||||
// target a status (either one of ours or a
|
||||
// remote) that requires approval for the fave.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
fave.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then send out the Like to
|
||||
// only the faved account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending reply.
|
||||
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending fave: %v", err)
|
||||
}
|
||||
|
||||
// Send Like to *remote* account inbox ONLY.
|
||||
if err := p.federate.Like(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error federating pending Like: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Like with the approval
|
||||
// URI attached.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the fave with approvedBy URI.
|
||||
approval, err := p.utils.approveFave(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving fave: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
|
|
@ -383,6 +505,61 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
|
|||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// If pending approval is true then status must
|
||||
// boost a status (either one of ours or a
|
||||
// remote) that requires approval for the boost.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
boost.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then send out the Announce to
|
||||
// only the boosted account (if it's remote),
|
||||
// and/or notify the account that's being
|
||||
// interacted with (if it's local): they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending announce.
|
||||
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending boost: %v", err)
|
||||
}
|
||||
|
||||
// Send Announce to *remote* account inbox ONLY.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error federating pending Announce: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and boost is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// sending out the Create with the approval
|
||||
// URI attached.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the boost with approvedBy URI.
|
||||
approval, err := p.utils.approveAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving boost: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
|
|
@ -874,3 +1051,18 @@ func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,11 +122,23 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
|||
|
||||
// ACCEPT SOMETHING
|
||||
case ap.ActivityAccept:
|
||||
switch fMsg.APObjectType { //nolint:gocritic
|
||||
switch fMsg.APObjectType {
|
||||
|
||||
// ACCEPT FOLLOW
|
||||
// ACCEPT (pending) FOLLOW
|
||||
case ap.ActivityFollow:
|
||||
return p.fediAPI.AcceptFollow(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) LIKE
|
||||
case ap.ActivityLike:
|
||||
return p.fediAPI.AcceptLike(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) REPLY
|
||||
case ap.ObjectNote:
|
||||
return p.fediAPI.AcceptReply(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) ANNOUNCE
|
||||
case ap.ActivityAnnounce:
|
||||
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
|
||||
}
|
||||
|
||||
// DELETE SOMETHING
|
||||
|
|
@ -216,6 +228,52 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
|||
return nil
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// status must reply to a LOCAL status
|
||||
// that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
status.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending reply.
|
||||
if err := p.surface.notifyPendingReply(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending reply: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the status with approvedBy URI.
|
||||
approval, err := p.utils.approveReply(ctx, status)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving reply: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the remote account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, status); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
|
|
@ -348,6 +406,52 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
|||
return gtserror.Newf("error populating status fave: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// fave must target a LOCAL status
|
||||
// that requires approval for the fave.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
fave.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending fave.
|
||||
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending fave: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the fave with approvedBy URI.
|
||||
approval, err := p.utils.approveFave(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving fave: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
|
|
@ -365,8 +469,9 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
|||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Dereference status that this boosts, note
|
||||
// that this will handle storing the boost in
|
||||
// Dereference into a boost wrapper status.
|
||||
//
|
||||
// Note: this will handle storing the boost in
|
||||
// the db, and dereferencing the target status
|
||||
// ancestors / descendants where appropriate.
|
||||
var err error
|
||||
|
|
@ -376,8 +481,10 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
|||
fMsg.Receiving.Username,
|
||||
)
|
||||
if err != nil {
|
||||
if gtserror.IsUnretrievable(err) {
|
||||
// Boosted status domain blocked, nothing to do.
|
||||
if gtserror.IsUnretrievable(err) ||
|
||||
gtserror.NotPermitted(err) {
|
||||
// Boosted status domain blocked, or
|
||||
// otherwise not permitted, nothing to do.
|
||||
log.Debugf(ctx, "skipping announce: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -386,6 +493,52 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
|||
return gtserror.Newf("error dereferencing announce: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// boost must target a LOCAL status
|
||||
// that requires approval for the boost.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
boost.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending announce.
|
||||
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending boost: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// Put approval in the database and
|
||||
// update the boost with approvedBy URI.
|
||||
approval, err := p.utils.approveAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error pre-approving boost: %w", err)
|
||||
}
|
||||
|
||||
// Send out the approval as Accept.
|
||||
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the remote account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
|
|
@ -549,6 +702,68 @@ func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg *messages.FromFediAPI)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
// TODO: Add something here if we ever implement sending out Likes to
|
||||
// followers more broadly and not just the owner of the Liked status.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
status, ok := fMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, status.Account, status); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
}
|
||||
|
||||
// Timeline and notify the status.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Interaction counts changed on the replied-to status;
|
||||
// uncache the prepared version from all timelines.
|
||||
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
|
||||
|
||||
// Send out the reply again, fully this time.
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, boost.Account, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
}
|
||||
|
||||
// Timeline and notify the boost wrapper status.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Interaction counts changed on the boosted status;
|
||||
// uncache the prepared version from all timelines.
|
||||
p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
|
||||
|
||||
// Send out the boost again, fully this time.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
// Cast the existing Status model attached to msg.
|
||||
existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
|
||||
|
|
|
|||
|
|
@ -32,6 +32,62 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// notifyPendingReply notifies the account replied-to
|
||||
// by the given status that they have a new reply,
|
||||
// and that approval is pending.
|
||||
func (s *Surface) notifyPendingReply(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
// Beforehand, ensure the passed status is fully populated.
|
||||
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
if status.InReplyToAccount.IsRemote() {
|
||||
// Don't notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
}
|
||||
|
||||
if status.AccountID == status.InReplyToAccountID {
|
||||
// Don't notify
|
||||
// self-replies.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure thread not muted
|
||||
// by replied-to account.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
status.ThreadID,
|
||||
status.InReplyToAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", status.ThreadID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// The replied-to account
|
||||
// has muted the thread.
|
||||
// Don't pester them.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify mentioned
|
||||
// by status author.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingReply,
|
||||
status.InReplyToAccount,
|
||||
status.Account,
|
||||
status.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying replied-to account %s: %w", status.InReplyToAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyMentions iterates through mentions on the
|
||||
// given status, and notifies each mentioned account
|
||||
// that they have a new mention.
|
||||
|
|
@ -181,36 +237,13 @@ func (s *Surface) notifyFave(
|
|||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) error {
|
||||
if fave.TargetAccountID == fave.AccountID {
|
||||
// Self-fave, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status fave is fully populated.
|
||||
if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error populating fave %s: %w", fave.ID, err)
|
||||
}
|
||||
|
||||
if fave.TargetAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure favee hasn't
|
||||
// muted the thread.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
fave.Status.ThreadID,
|
||||
fave.TargetAccountID,
|
||||
)
|
||||
notifyable, err := s.notifyableFave(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Favee doesn't want
|
||||
// notifs for this thread.
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -228,31 +261,167 @@ func (s *Surface) notifyFave(
|
|||
return nil
|
||||
}
|
||||
|
||||
// notifyPendingFave notifies the target of the
|
||||
// given fave that their status has been faved
|
||||
// and that approval is required.
|
||||
func (s *Surface) notifyPendingFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) error {
|
||||
notifyable, err := s.notifyableFave(ctx, fave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of fave by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingFave,
|
||||
fave.TargetAccount,
|
||||
fave.Account,
|
||||
fave.StatusID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyableFave checks that the given
|
||||
// fave should be notified, taking account
|
||||
// of localness of receiving account, and mutes.
|
||||
func (s *Surface) notifyableFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) (bool, error) {
|
||||
if fave.TargetAccountID == fave.AccountID {
|
||||
// Self-fave, nothing to do.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status fave is fully populated.
|
||||
if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
return false, gtserror.Newf("error populating fave %s: %w", fave.ID, err)
|
||||
}
|
||||
|
||||
if fave.TargetAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Ensure favee hasn't
|
||||
// muted the thread.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
fave.Status.ThreadID,
|
||||
fave.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Favee doesn't want
|
||||
// notifs for this thread.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// notifyAnnounce notifies the status boost target
|
||||
// account that their status has been boosted.
|
||||
func (s *Surface) notifyAnnounce(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
boost *gtsmodel.Status,
|
||||
) error {
|
||||
notifyable, err := s.notifyableAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationReblog,
|
||||
boost.BoostOfAccount,
|
||||
boost.Account,
|
||||
boost.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyPendingAnnounce notifies the status boost
|
||||
// target account that their status has been boosted,
|
||||
// and that the boost requires approval.
|
||||
func (s *Surface) notifyPendingAnnounce(
|
||||
ctx context.Context,
|
||||
boost *gtsmodel.Status,
|
||||
) error {
|
||||
notifyable, err := s.notifyableAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingReblog,
|
||||
boost.BoostOfAccount,
|
||||
boost.Account,
|
||||
boost.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyableAnnounce checks that the given
|
||||
// announce should be notified, taking account
|
||||
// of localness of receiving account, and mutes.
|
||||
func (s *Surface) notifyableAnnounce(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
if status.BoostOfID == "" {
|
||||
// Not a boost, nothing to do.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if status.BoostOfAccountID == status.AccountID {
|
||||
// Self-boost, nothing to do.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status is fully populated.
|
||||
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
return false, gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
if status.BoostOfAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Ensure boostee hasn't
|
||||
|
|
@ -264,27 +433,16 @@ func (s *Surface) notifyAnnounce(
|
|||
)
|
||||
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err)
|
||||
return false, gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Boostee doesn't want
|
||||
// notifs for this thread.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationReblog,
|
||||
status.BoostOfAccount,
|
||||
status.Account,
|
||||
status.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying status author %s: %w", status.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,13 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// util provides util functions used by both
|
||||
|
|
@ -498,3 +501,129 @@ func (u *utils) decrementFollowRequestsCount(
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// approveFave stores + returns an
|
||||
// interactionApproval for a fave.
|
||||
func (u *utils) approveFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: fave.TargetAccountID,
|
||||
Account: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the fave itself as now approved.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.PreApproved = false
|
||||
fave.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating status fave: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
||||
// approveReply stores + returns an
|
||||
// interactionApproval for a reply.
|
||||
func (u *utils) approveReply(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: status.InReplyToAccountID,
|
||||
Account: status.InReplyToAccount,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractingAccount: status.Account,
|
||||
InteractionURI: status.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the status itself as now approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.PreApproved = false
|
||||
status.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating status: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
||||
// approveAnnounce stores + returns an
|
||||
// interactionApproval for an announce.
|
||||
func (u *utils) approveAnnounce(
|
||||
ctx context.Context,
|
||||
boost *gtsmodel.Status,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: boost.BoostOfAccountID,
|
||||
Account: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the status itself as now approved.
|
||||
boost.PendingApproval = util.Ptr(false)
|
||||
boost.PreApproved = false
|
||||
boost.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
boost,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating boost wrapper status: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const (
|
|||
follow = "follow"
|
||||
blocks = "blocks"
|
||||
reports = "reports"
|
||||
accepts = "accepts"
|
||||
|
||||
schemes = `(http|https)://` // Allowed URI protocols for parsing links in text.
|
||||
alphaNumeric = `\p{L}\p{M}*|\p{N}` // A single number or script character in any language, including chars with accents.
|
||||
|
|
@ -71,6 +72,7 @@ const (
|
|||
followPath = userPathPrefix + `/` + follow + `/(` + ulid + `)$`
|
||||
likePath = userPathPrefix + `/` + liked + `/(` + ulid + `)$`
|
||||
statusesPath = userPathPrefix + `/` + statuses + `/(` + ulid + `)$`
|
||||
acceptsPath = userPathPrefix + `/` + accepts + `/(` + ulid + `)$`
|
||||
blockPath = userPathPrefix + `/` + blocks + `/(` + ulid + `)$`
|
||||
reportPath = `^/?` + reports + `/(` + ulid + `)$`
|
||||
filePath = `^/?(` + ulid + `)/([a-z]+)/([a-z]+)/(` + ulid + `)\.([a-z0-9]+)$`
|
||||
|
|
@ -158,6 +160,10 @@ var (
|
|||
// from eg /reports/01GP3AWY4CRDVRNZKW0TEAMB5R
|
||||
ReportPath = regexp.MustCompile(reportPath)
|
||||
|
||||
// ReportPath parses a path that validates and captures the username part and the ulid part
|
||||
// from eg /users/example_username/accepts/01GP3AWY4CRDVRNZKW0TEAMB5R
|
||||
AcceptsPath = regexp.MustCompile(acceptsPath)
|
||||
|
||||
// FilePath parses a file storage path of the form [ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]
|
||||
// eg 01F8MH1H7YV1Z7D2C8K2730QBF/attachment/small/01F8MH8RMYQ6MSNY3JM2XT1CQ5.jpeg
|
||||
// It captures the account id, media type, media size, file name, and file extension, eg
|
||||
|
|
|
|||
|
|
@ -204,6 +204,38 @@ func (c *controller) dereferenceLocalUser(ctx context.Context, iri *url.URL) (*h
|
|||
return rsp, nil
|
||||
}
|
||||
|
||||
// dereferenceLocalAccept is a shortcut to dereference an accept created
|
||||
// by an account on this instance, without making any external api/http calls.
|
||||
//
|
||||
// It is passed to new transports, and should only be invoked when the iri.Host == this host.
|
||||
func (c *controller) dereferenceLocalAccept(ctx context.Context, iri *url.URL) (*http.Response, error) {
|
||||
accept, err := c.fedDB.GetAccept(ctx, iri)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if accept == nil {
|
||||
// Return a generic 404 not found response.
|
||||
rsp := craftResponse(iri, http.StatusNotFound)
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
i, err := ap.Serialize(accept)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return a response with AS data as body.
|
||||
rsp := craftResponse(iri, http.StatusOK)
|
||||
rsp.Body = io.NopCloser(bytes.NewReader(b))
|
||||
return rsp, nil
|
||||
}
|
||||
|
||||
func craftResponse(url *url.URL, code int) *http.Response {
|
||||
rsp := new(http.Response)
|
||||
rsp.Request = new(http.Request)
|
||||
|
|
|
|||
|
|
@ -29,17 +29,26 @@ import (
|
|||
)
|
||||
|
||||
func (t *transport) Dereference(ctx context.Context, iri *url.URL) (*http.Response, error) {
|
||||
// if the request is to us, we can shortcut for certain URIs rather than going through
|
||||
// the normal request flow, thereby saving time and energy
|
||||
// If the request is to us, we can shortcut for
|
||||
// certain URIs rather than going through the normal
|
||||
// request flow, thereby saving time and energy.
|
||||
if iri.Host == config.GetHost() {
|
||||
if uris.IsFollowersPath(iri) {
|
||||
// the request is for followers of one of our accounts, which we can shortcut
|
||||
return t.controller.dereferenceLocalFollowers(ctx, iri)
|
||||
}
|
||||
switch {
|
||||
|
||||
if uris.IsUserPath(iri) {
|
||||
// the request is for one of our accounts, which we can shortcut
|
||||
case uris.IsFollowersPath(iri):
|
||||
// The request is for followers of one of
|
||||
// our accounts, which we can shortcut.
|
||||
return t.controller.dereferenceLocalFollowers(ctx, iri)
|
||||
|
||||
case uris.IsUserPath(iri):
|
||||
// The request is for one of our
|
||||
// accounts, which we can shortcut.
|
||||
return t.controller.dereferenceLocalUser(ctx, iri)
|
||||
|
||||
case uris.IsAcceptsPath(iri):
|
||||
// The request is for an Accept on
|
||||
// our instance, which we can shortcut.
|
||||
return t.controller.dereferenceLocalAccept(ctx, iri)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -393,13 +393,23 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab
|
|||
return nil, gtserror.SetMalformed(err)
|
||||
}
|
||||
|
||||
// Advanced visibility toggles for this status.
|
||||
//
|
||||
// TODO: a lot of work to be done here -- a new type
|
||||
// needs to be created for this in go-fed/activity.
|
||||
// Until this is implemented, assume all true.
|
||||
// Status was sent to us or dereffed
|
||||
// by us so it must be federated.
|
||||
status.Federated = util.Ptr(true)
|
||||
|
||||
// Derive interaction policy for this status.
|
||||
status.InteractionPolicy = ap.ExtractInteractionPolicy(
|
||||
statusable,
|
||||
status.Account,
|
||||
)
|
||||
|
||||
// Set approvedByURI if present,
|
||||
// for later dereferencing.
|
||||
approvedByURI := ap.GetApprovedBy(statusable)
|
||||
if approvedByURI != nil {
|
||||
status.ApprovedByURI = approvedByURI.String()
|
||||
}
|
||||
|
||||
// status.Sensitive
|
||||
sensitive := ap.ExtractSensitive(statusable)
|
||||
status.Sensitive = &sensitive
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// AccountToAS converts a gts model account into an activity streams person, suitable for federation
|
||||
|
|
@ -672,6 +673,38 @@ func (c *Converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (ap.Stat
|
|||
sensitiveProp.AppendXMLSchemaBoolean(*s.Sensitive)
|
||||
status.SetActivityStreamsSensitive(sensitiveProp)
|
||||
|
||||
// interactionPolicy
|
||||
var p *gtsmodel.InteractionPolicy
|
||||
if s.InteractionPolicy != nil {
|
||||
// Use InteractionPolicy
|
||||
// set on the status.
|
||||
p = s.InteractionPolicy
|
||||
} else {
|
||||
// Fall back to default policy
|
||||
// for the status's visibility.
|
||||
p = gtsmodel.DefaultInteractionPolicyFor(s.Visibility)
|
||||
}
|
||||
policy, err := c.InteractionPolicyToASInteractionPolicy(ctx, p, s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating interactionPolicy: %w", err)
|
||||
}
|
||||
|
||||
policyProp := streams.NewGoToSocialInteractionPolicyProperty()
|
||||
policyProp.AppendGoToSocialInteractionPolicy(policy)
|
||||
status.SetGoToSocialInteractionPolicy(policyProp)
|
||||
|
||||
// Parse + set approvedBy.
|
||||
if s.ApprovedByURI != "" {
|
||||
approvedBy, err := url.Parse(s.ApprovedByURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
||||
}
|
||||
|
||||
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
||||
approvedByProp.Set(approvedBy)
|
||||
status.SetGoToSocialApprovedBy(approvedByProp)
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
|
|
@ -1169,6 +1202,18 @@ func (c *Converter) FaveToAS(ctx context.Context, f *gtsmodel.StatusFave) (vocab
|
|||
toProp.AppendIRI(toIRI)
|
||||
like.SetActivityStreamsTo(toProp)
|
||||
|
||||
// Parse + set approvedBy.
|
||||
if f.ApprovedByURI != "" {
|
||||
approvedBy, err := url.Parse(f.ApprovedByURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
||||
}
|
||||
|
||||
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
||||
approvedByProp.Set(approvedBy)
|
||||
like.SetGoToSocialApprovedBy(approvedByProp)
|
||||
}
|
||||
|
||||
return like, nil
|
||||
}
|
||||
|
||||
|
|
@ -1247,6 +1292,18 @@ func (c *Converter) BoostToAS(ctx context.Context, boostWrapperStatus *gtsmodel.
|
|||
|
||||
announce.SetActivityStreamsCc(ccProp)
|
||||
|
||||
// Parse + set approvedBy.
|
||||
if boostWrapperStatus.ApprovedByURI != "" {
|
||||
approvedBy, err := url.Parse(boostWrapperStatus.ApprovedByURI)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing approvedBy: %w", err)
|
||||
}
|
||||
|
||||
approvedByProp := streams.NewGoToSocialApprovedByProperty()
|
||||
approvedByProp.Set(approvedBy)
|
||||
announce.SetGoToSocialApprovedBy(approvedByProp)
|
||||
}
|
||||
|
||||
return announce, nil
|
||||
}
|
||||
|
||||
|
|
@ -1724,3 +1781,227 @@ func (c *Converter) PollVoteToASCreate(
|
|||
|
||||
return create, nil
|
||||
}
|
||||
|
||||
// populateValuesForProp appends the given PolicyValues
|
||||
// to the given property, for the given status.
|
||||
func populateValuesForProp[T ap.WithIRI](
|
||||
prop ap.Property[T],
|
||||
status *gtsmodel.Status,
|
||||
urns gtsmodel.PolicyValues,
|
||||
) error {
|
||||
iriStrs := make([]string, 0)
|
||||
|
||||
for _, urn := range urns {
|
||||
switch urn {
|
||||
|
||||
case gtsmodel.PolicyValueAuthor:
|
||||
iriStrs = append(iriStrs, status.Account.URI)
|
||||
|
||||
case gtsmodel.PolicyValueMentioned:
|
||||
for _, m := range status.Mentions {
|
||||
iriStrs = append(iriStrs, m.TargetAccount.URI)
|
||||
}
|
||||
|
||||
case gtsmodel.PolicyValueFollowing:
|
||||
iriStrs = append(iriStrs, status.Account.FollowingURI)
|
||||
|
||||
case gtsmodel.PolicyValueFollowers:
|
||||
iriStrs = append(iriStrs, status.Account.FollowersURI)
|
||||
|
||||
case gtsmodel.PolicyValuePublic:
|
||||
iriStrs = append(iriStrs, pub.PublicActivityPubIRI)
|
||||
|
||||
default:
|
||||
iriStrs = append(iriStrs, string(urn))
|
||||
}
|
||||
}
|
||||
|
||||
// Deduplicate the iri strings to
|
||||
// make sure we're not parsing + adding
|
||||
// the same string multiple times.
|
||||
iriStrs = util.Deduplicate(iriStrs)
|
||||
|
||||
// Append them to the property.
|
||||
for _, iriStr := range iriStrs {
|
||||
iri, err := url.Parse(iriStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prop.AppendIRI(iri)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InteractionPolicyToASInteractionPolicy returns a
|
||||
// GoToSocial interaction policy suitable for federation.
|
||||
func (c *Converter) InteractionPolicyToASInteractionPolicy(
|
||||
ctx context.Context,
|
||||
interactionPolicy *gtsmodel.InteractionPolicy,
|
||||
status *gtsmodel.Status,
|
||||
) (vocab.GoToSocialInteractionPolicy, error) {
|
||||
policy := streams.NewGoToSocialInteractionPolicy()
|
||||
|
||||
/*
|
||||
CAN LIKE
|
||||
*/
|
||||
|
||||
// Build canLike
|
||||
canLike := streams.NewGoToSocialCanLike()
|
||||
|
||||
// Build canLike.always
|
||||
canLikeAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
||||
if err := populateValuesForProp(
|
||||
canLikeAlwaysProp,
|
||||
status,
|
||||
interactionPolicy.CanLike.Always,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canLike.always: %w", err)
|
||||
}
|
||||
|
||||
// Set canLike.always
|
||||
canLike.SetGoToSocialAlways(canLikeAlwaysProp)
|
||||
|
||||
// Build canLike.approvalRequired
|
||||
canLikeApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
||||
if err := populateValuesForProp(
|
||||
canLikeApprovalRequiredProp,
|
||||
status,
|
||||
interactionPolicy.CanLike.WithApproval,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canLike.approvalRequired: %w", err)
|
||||
}
|
||||
|
||||
// Set canLike.approvalRequired.
|
||||
canLike.SetGoToSocialApprovalRequired(canLikeApprovalRequiredProp)
|
||||
|
||||
// Set canLike on the policy.
|
||||
canLikeProp := streams.NewGoToSocialCanLikeProperty()
|
||||
canLikeProp.AppendGoToSocialCanLike(canLike)
|
||||
policy.SetGoToSocialCanLike(canLikeProp)
|
||||
|
||||
/*
|
||||
CAN REPLY
|
||||
*/
|
||||
|
||||
// Build canReply
|
||||
canReply := streams.NewGoToSocialCanReply()
|
||||
|
||||
// Build canReply.always
|
||||
canReplyAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
||||
if err := populateValuesForProp(
|
||||
canReplyAlwaysProp,
|
||||
status,
|
||||
interactionPolicy.CanReply.Always,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canReply.always: %w", err)
|
||||
}
|
||||
|
||||
// Set canReply.always
|
||||
canReply.SetGoToSocialAlways(canReplyAlwaysProp)
|
||||
|
||||
// Build canReply.approvalRequired
|
||||
canReplyApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
||||
if err := populateValuesForProp(
|
||||
canReplyApprovalRequiredProp,
|
||||
status,
|
||||
interactionPolicy.CanReply.WithApproval,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canReply.approvalRequired: %w", err)
|
||||
}
|
||||
|
||||
// Set canReply.approvalRequired.
|
||||
canReply.SetGoToSocialApprovalRequired(canReplyApprovalRequiredProp)
|
||||
|
||||
// Set canReply on the policy.
|
||||
canReplyProp := streams.NewGoToSocialCanReplyProperty()
|
||||
canReplyProp.AppendGoToSocialCanReply(canReply)
|
||||
policy.SetGoToSocialCanReply(canReplyProp)
|
||||
|
||||
/*
|
||||
CAN ANNOUNCE
|
||||
*/
|
||||
|
||||
// Build canAnnounce
|
||||
canAnnounce := streams.NewGoToSocialCanAnnounce()
|
||||
|
||||
// Build canAnnounce.always
|
||||
canAnnounceAlwaysProp := streams.NewGoToSocialAlwaysProperty()
|
||||
if err := populateValuesForProp(
|
||||
canAnnounceAlwaysProp,
|
||||
status,
|
||||
interactionPolicy.CanAnnounce.Always,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canAnnounce.always: %w", err)
|
||||
}
|
||||
|
||||
// Set canAnnounce.always
|
||||
canAnnounce.SetGoToSocialAlways(canAnnounceAlwaysProp)
|
||||
|
||||
// Build canAnnounce.approvalRequired
|
||||
canAnnounceApprovalRequiredProp := streams.NewGoToSocialApprovalRequiredProperty()
|
||||
if err := populateValuesForProp(
|
||||
canAnnounceApprovalRequiredProp,
|
||||
status,
|
||||
interactionPolicy.CanAnnounce.WithApproval,
|
||||
); err != nil {
|
||||
return nil, gtserror.Newf("error setting canAnnounce.approvalRequired: %w", err)
|
||||
}
|
||||
|
||||
// Set canAnnounce.approvalRequired.
|
||||
canAnnounce.SetGoToSocialApprovalRequired(canAnnounceApprovalRequiredProp)
|
||||
|
||||
// Set canAnnounce on the policy.
|
||||
canAnnounceProp := streams.NewGoToSocialCanAnnounceProperty()
|
||||
canAnnounceProp.AppendGoToSocialCanAnnounce(canAnnounce)
|
||||
policy.SetGoToSocialCanAnnounce(canAnnounceProp)
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// InteractionApprovalToASAccept converts a *gtsmodel.InteractionApproval
|
||||
// to an ActivityStreams Accept, addressed to the interacting account.
|
||||
func (c *Converter) InteractionApprovalToASAccept(
|
||||
ctx context.Context,
|
||||
approval *gtsmodel.InteractionApproval,
|
||||
) (vocab.ActivityStreamsAccept, error) {
|
||||
accept := streams.NewActivityStreamsAccept()
|
||||
|
||||
acceptID, err := url.Parse(approval.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid accept uri: %w", err)
|
||||
}
|
||||
|
||||
actorIRI, err := url.Parse(approval.Account.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid account uri: %w", err)
|
||||
}
|
||||
|
||||
objectIRI, err := url.Parse(approval.InteractionURI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid target uri: %w", err)
|
||||
}
|
||||
|
||||
toIRI, err := url.Parse(approval.InteractingAccount.URI)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
||||
}
|
||||
|
||||
// Set id to the URI of
|
||||
// interactionApproval.
|
||||
ap.SetJSONLDId(accept, acceptID)
|
||||
|
||||
// Actor is the account that
|
||||
// owns the approval / accept.
|
||||
ap.AppendActorIRIs(accept, actorIRI)
|
||||
|
||||
// Object is the interaction URI.
|
||||
ap.AppendObjectIRIs(accept, objectIRI)
|
||||
|
||||
// Address to the owner
|
||||
// of interaction URI.
|
||||
ap.AppendTo(accept, toIRI)
|
||||
|
||||
return accept, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
|
@ -46,14 +44,15 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'discoverable';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
|
||||
|
||||
suite.Equal(`: true,
|
||||
suite.Equal(`{
|
||||
"discoverable": true,
|
||||
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
|
||||
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"following": "http://localhost:8080/users/the_mighty_zork/following",
|
||||
|
|
@ -82,7 +81,7 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
|
|||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@the_mighty_zork"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
|
||||
|
|
@ -95,16 +94,15 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'attachment';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"attachment\"")[1]
|
||||
|
||||
fmt.Printf("\n\n\n%s\n\n\n", string(bytes))
|
||||
|
||||
suite.Equal(`: [
|
||||
suite.Equal(`{
|
||||
"attachment": [
|
||||
{
|
||||
"name": "should you follow me?",
|
||||
"type": "PropertyValue",
|
||||
|
|
@ -135,7 +133,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
|
|||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@1happyturtle"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
|
||||
|
|
@ -161,14 +159,15 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'alsoKnownAs';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"alsoKnownAs\"")[1]
|
||||
|
||||
suite.Equal(`: [
|
||||
suite.Equal(`{
|
||||
"alsoKnownAs": [
|
||||
"http://localhost:8080/users/1happyturtle"
|
||||
],
|
||||
"discoverable": true,
|
||||
|
|
@ -201,7 +200,7 @@ func (suite *InternalToASTestSuite) TestAccountToASAliasedAndMoved() {
|
|||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@the_mighty_zork"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
|
||||
|
|
@ -215,15 +214,16 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'attachment';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"attachment\"")[1]
|
||||
|
||||
// Despite only one field being set, attachments should still be a slice/array.
|
||||
suite.Equal(`: [
|
||||
suite.Equal(`{
|
||||
"attachment": [
|
||||
{
|
||||
"name": "should you follow me?",
|
||||
"type": "PropertyValue",
|
||||
|
|
@ -249,7 +249,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
|
|||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@1happyturtle"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
|
||||
|
|
@ -263,14 +263,15 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'discoverable';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
|
||||
|
||||
suite.Equal(`: true,
|
||||
suite.Equal(`{
|
||||
"discoverable": true,
|
||||
"featured": "http://localhost:8080/users/the_mighty_zork/collections/featured",
|
||||
"followers": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"following": "http://localhost:8080/users/the_mighty_zork/following",
|
||||
|
|
@ -309,7 +310,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
|
|||
},
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@the_mighty_zork"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
|
||||
|
|
@ -324,14 +325,15 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
|
|||
ser, err := ap.Serialize(asPerson)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// trim off everything up to 'discoverable';
|
||||
// this is necessary because the order of multiple 'context' entries is not determinate
|
||||
trimmed := strings.Split(string(bytes), "\"discoverable\"")[1]
|
||||
|
||||
suite.Equal(`: true,
|
||||
suite.Equal(`{
|
||||
"discoverable": true,
|
||||
"endpoints": {
|
||||
"sharedInbox": "http://localhost:8080/sharedInbox"
|
||||
},
|
||||
|
|
@ -363,7 +365,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
|
|||
"tag": [],
|
||||
"type": "Person",
|
||||
"url": "http://localhost:8080/@the_mighty_zork"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestStatusToAS() {
|
||||
|
|
@ -376,11 +378,14 @@ func (suite *InternalToASTestSuite) TestStatusToAS() {
|
|||
ser, err := ap.Serialize(asStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"attachment": [],
|
||||
"attributedTo": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
|
|
@ -389,6 +394,26 @@ func (suite *InternalToASTestSuite) TestStatusToAS() {
|
|||
"en": "hello everyone!"
|
||||
},
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"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": []
|
||||
}
|
||||
},
|
||||
"published": "2021-10-20T12:40:37+02:00",
|
||||
"replies": {
|
||||
"first": {
|
||||
|
|
@ -420,14 +445,15 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() {
|
|||
ser, err := ap.Serialize(asStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// we can't be sure in what order the two context entries --
|
||||
// http://joinmastodon.org/ns, https://www.w3.org/ns/activitystreams --
|
||||
// will appear, so trim them out of the string for consistency
|
||||
trimmed := strings.SplitAfter(string(bytes), `"attachment":`)[1]
|
||||
suite.Equal(` [
|
||||
suite.Equal(`{
|
||||
"attachment": [
|
||||
{
|
||||
"blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj",
|
||||
"mediaType": "image/jpeg",
|
||||
|
|
@ -443,6 +469,26 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() {
|
|||
"en": "hello world! #welcome ! first post on the instance :rainbow: !"
|
||||
},
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"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": []
|
||||
}
|
||||
},
|
||||
"published": "2021-10-20T11:36:45Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
|
|
@ -477,7 +523,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() {
|
|||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
|
||||
|
|
@ -492,14 +538,15 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
|
|||
ser, err := ap.Serialize(asStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
// we can't be sure in what order the two context entries --
|
||||
// http://joinmastodon.org/ns, https://www.w3.org/ns/activitystreams --
|
||||
// will appear, so trim them out of the string for consistency
|
||||
trimmed := strings.SplitAfter(string(bytes), `"attachment":`)[1]
|
||||
suite.Equal(` [
|
||||
suite.Equal(`{
|
||||
"attachment": [
|
||||
{
|
||||
"blurhash": "LIIE|gRj00WB-;j[t7j[4nWBj[Rj",
|
||||
"mediaType": "image/jpeg",
|
||||
|
|
@ -515,6 +562,26 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
|
|||
"en": "hello world! #welcome ! first post on the instance :rainbow: !"
|
||||
},
|
||||
"id": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||
"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": []
|
||||
}
|
||||
},
|
||||
"published": "2021-10-20T11:36:45Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
|
|
@ -549,7 +616,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
|
|||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note",
|
||||
"url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}`, trimmed)
|
||||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestStatusToASWithMentions() {
|
||||
|
|
@ -565,11 +632,14 @@ func (suite *InternalToASTestSuite) TestStatusToASWithMentions() {
|
|||
ser, err := ap.Serialize(asStatus)
|
||||
suite.NoError(err)
|
||||
|
||||
// Drop "@context" property as
|
||||
// the ordering is non-determinate.
|
||||
delete(ser, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(ser, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"attachment": [],
|
||||
"attributedTo": "http://localhost:8080/users/admin",
|
||||
"cc": [
|
||||
|
|
@ -582,6 +652,26 @@ func (suite *InternalToASTestSuite) TestStatusToASWithMentions() {
|
|||
},
|
||||
"id": "http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0",
|
||||
"inReplyTo": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"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": []
|
||||
}
|
||||
},
|
||||
"published": "2021-11-20T13:32:16Z",
|
||||
"replies": {
|
||||
"first": {
|
||||
|
|
@ -967,6 +1057,51 @@ func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
|
|||
}`, string(bytes))
|
||||
}
|
||||
|
||||
func (suite *InternalToASTestSuite) TestInteractionApprovalToASAccept() {
|
||||
acceptingAccount := suite.testAccounts["local_account_1"]
|
||||
interactingAccount := suite.testAccounts["remote_account_1"]
|
||||
|
||||
interactionApproval := >smodel.InteractionApproval{
|
||||
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
UpdatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
|
||||
AccountID: acceptingAccount.ID,
|
||||
Account: acceptingAccount,
|
||||
InteractingAccountID: interactingAccount.ID,
|
||||
InteractingAccount: interactingAccount,
|
||||
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
InteractionType: gtsmodel.InteractionAnnounce,
|
||||
URI: "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||
}
|
||||
|
||||
accept, err := suite.typeconverter.InteractionApprovalToASAccept(
|
||||
context.Background(),
|
||||
interactionApproval,
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
i, err := ap.Serialize(accept)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(i, "", " ")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||
"object": "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||
"to": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"type": "Accept"
|
||||
}`, string(b))
|
||||
}
|
||||
|
||||
func TestInternalToASTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(InternalToASTestSuite))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,11 +72,14 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
|||
createI, err := ap.Serialize(create)
|
||||
suite.NoError(err)
|
||||
|
||||
// Chop off @context since
|
||||
// ordering is non-determinate.
|
||||
delete(createI, "@context")
|
||||
|
||||
bytes, err := json.MarshalIndent(createI, "", " ")
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Equal(`{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"actor": "http://localhost:8080/users/the_mighty_zork",
|
||||
"cc": "http://localhost:8080/users/the_mighty_zork/followers",
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity#Create",
|
||||
|
|
@ -89,6 +92,26 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
|
|||
"en": "hello everyone!"
|
||||
},
|
||||
"id": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
"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": []
|
||||
}
|
||||
},
|
||||
"published": "2021-10-20T12:40:37+02:00",
|
||||
"replies": {
|
||||
"first": {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ const (
|
|||
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
||||
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
||||
TagsPath = "tags" // TagsPath represents the activitypub tags location
|
||||
AcceptsPath = "accepts" // AcceptsPath represents the activitypub accepts location
|
||||
)
|
||||
|
||||
// UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc.
|
||||
|
|
@ -136,6 +137,14 @@ func GenerateURIForEmailConfirm(token string) string {
|
|||
return fmt.Sprintf("%s://%s/%s?token=%s", protocol, host, ConfirmEmailPath, token)
|
||||
}
|
||||
|
||||
// GenerateURIForAccept returns the AP URI for a new accept activity -- something like:
|
||||
// https://example.org/users/whatever_user/accepts/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||
func GenerateURIForAccept(username string, thisAcceptID string) string {
|
||||
protocol := config.GetProtocol()
|
||||
host := config.GetHost()
|
||||
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, AcceptsPath, thisAcceptID)
|
||||
}
|
||||
|
||||
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
|
||||
func GenerateURIsForAccount(username string) *UserURIs {
|
||||
protocol := config.GetProtocol()
|
||||
|
|
@ -317,6 +326,11 @@ func IsReportPath(id *url.URL) bool {
|
|||
return regexes.ReportPath.MatchString(id.Path)
|
||||
}
|
||||
|
||||
// IsAcceptsPath returns true if the given URL path corresponds to eg /users/example_username/accepts/SOME_ULID_OF_AN_ACCEPT
|
||||
func IsAcceptsPath(id *url.URL) bool {
|
||||
return regexes.AcceptsPath.MatchString(id.Path)
|
||||
}
|
||||
|
||||
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
||||
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||
matches := regexes.StatusesPath.FindStringSubmatch(id.Path)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue