mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-02 13:02:56 +00:00
[feature] Implement deliveryRecipientPreSort
to prioritize delivery to mentioned accounts (#3668)
* weeeeenus * update to latest activity * update to use latest release tag of superseriousbusiness/activity --------- Co-authored-by: kim <grufwub@gmail.com>
This commit is contained in:
parent
9048290948
commit
65fb8abd42
2
go.mod
2
go.mod
|
@ -70,7 +70,7 @@ require (
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
github.com/spf13/viper v1.19.0
|
github.com/spf13/viper v1.19.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
github.com/superseriousbusiness/activity v1.9.0-gts
|
github.com/superseriousbusiness/activity v1.10.0-gts
|
||||||
github.com/superseriousbusiness/httpsig v1.2.0-SSB
|
github.com/superseriousbusiness/httpsig v1.2.0-SSB
|
||||||
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
|
||||||
github.com/tdewolff/minify/v2 v2.21.2
|
github.com/tdewolff/minify/v2 v2.21.2
|
||||||
|
|
4
go.sum
generated
4
go.sum
generated
|
@ -533,8 +533,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
github.com/superseriousbusiness/activity v1.9.0-gts h1:qWMDeiGdnVi+XG7CfuM7ET87qe9adousU6utWItBX/o=
|
github.com/superseriousbusiness/activity v1.10.0-gts h1:uYIHU0/jDpLxj0lA3Jg24lM8p3X/Vb3J7hn3yQJR+C8=
|
||||||
github.com/superseriousbusiness/activity v1.9.0-gts/go.mod h1:9l74ZCv8zw07vipNMzahq8oQZt2xPaJZ+L+gLicQntQ=
|
github.com/superseriousbusiness/activity v1.10.0-gts/go.mod h1:9l74ZCv8zw07vipNMzahq8oQZt2xPaJZ+L+gLicQntQ=
|
||||||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe h1:ksl2oCx/Qo8sNDc3Grb8WGKBM9nkvhCm25uvlT86azE=
|
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe h1:ksl2oCx/Qo8sNDc3Grb8WGKBM9nkvhCm25uvlT86azE=
|
||||||
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
|
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe/go.mod h1:gH4P6gN1V+wmIw5o97KGaa1RgXB/tVpC2UNzijhg3E4=
|
||||||
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB h1:8psprYSK1KdOSH7yQ4PbJq0YYaGQY+gzdW/B0ExDb/8=
|
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB h1:8psprYSK1KdOSH7yQ4PbJq0YYaGQY+gzdW/B0ExDb/8=
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
|
||||||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
|
@ -30,9 +31,11 @@
|
||||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
)
|
)
|
||||||
|
|
||||||
// federatingActor wraps the pub.FederatingActor
|
// federatingActor wraps the pub.FederatingActor
|
||||||
|
@ -42,10 +45,63 @@ type federatingActor struct {
|
||||||
wrapped pub.FederatingActor
|
wrapped pub.FederatingActor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func deliveryRecipientPreSort(actorAndCollectionIRIs []*url.URL) []*url.URL {
|
||||||
|
var (
|
||||||
|
thisHost = config.GetHost()
|
||||||
|
thisAcctDomain = config.GetAccountDomain()
|
||||||
|
)
|
||||||
|
|
||||||
|
slices.SortFunc(
|
||||||
|
actorAndCollectionIRIs,
|
||||||
|
func(a *url.URL, b *url.URL) int {
|
||||||
|
// We want to sort by putting more specific actor URIs *before* collection URIs.
|
||||||
|
// Since the only collection URIs we ever address are our own followers URIs, we
|
||||||
|
// can just use host and regexes to identify these collections, and shove them
|
||||||
|
// to the back of the slice. This ensures that directly addressed (ie., mentioned)
|
||||||
|
// accounts get delivery-attempted *first*, and then delivery attempts move on to
|
||||||
|
// followers of the author. This should have the effect of making conversation
|
||||||
|
/// threads feel more snappy, as replies will be sent quicker to participants.
|
||||||
|
var (
|
||||||
|
aIsFollowers = (a.Host == thisHost || a.Host == thisAcctDomain) && uris.IsFollowersPath(a)
|
||||||
|
bIsFollowers = (b.Host == thisHost || b.Host == thisAcctDomain) && uris.IsFollowersPath(b)
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case aIsFollowers == bIsFollowers:
|
||||||
|
// Both followers URIs or
|
||||||
|
// both not followers URIs,
|
||||||
|
// order doesn't matter.
|
||||||
|
return 0
|
||||||
|
|
||||||
|
case aIsFollowers:
|
||||||
|
// a is followers
|
||||||
|
// URI, b is not.
|
||||||
|
//
|
||||||
|
// Sort b before a.
|
||||||
|
return 1
|
||||||
|
|
||||||
|
default:
|
||||||
|
// b is followers
|
||||||
|
// URI, a is not.
|
||||||
|
//
|
||||||
|
// Sort a before b.
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return actorAndCollectionIRIs
|
||||||
|
}
|
||||||
|
|
||||||
// newFederatingActor returns a federatingActor.
|
// newFederatingActor returns a federatingActor.
|
||||||
func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
|
func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
|
||||||
sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock)
|
sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock)
|
||||||
sideEffectActor.Serialize = ap.Serialize // hook in our own custom Serialize function
|
|
||||||
|
// Hook in our own custom Serialize function.
|
||||||
|
sideEffectActor.Serialize = ap.Serialize
|
||||||
|
|
||||||
|
// Hook in our own custom recipient pre-sort function.
|
||||||
|
sideEffectActor.DeliveryRecipientPreSort = deliveryRecipientPreSort
|
||||||
|
|
||||||
return &federatingActor{
|
return &federatingActor{
|
||||||
sideEffectActor: sideEffectActor,
|
sideEffectActor: sideEffectActor,
|
||||||
|
|
399
vendor/github.com/superseriousbusiness/activity/pub/side_effect_actor.go
generated
vendored
399
vendor/github.com/superseriousbusiness/activity/pub/side_effect_actor.go
generated
vendored
|
@ -20,25 +20,58 @@
|
||||||
// Note that when using the SideEffectActor with an application that good-faith
|
// Note that when using the SideEffectActor with an application that good-faith
|
||||||
// implements its required interfaces, the ActivityPub specification is
|
// implements its required interfaces, the ActivityPub specification is
|
||||||
// guaranteed to be correctly followed.
|
// guaranteed to be correctly followed.
|
||||||
//
|
|
||||||
// When doing deliveries to remote servers via the s2s protocol, the side effect
|
|
||||||
// actor will by default use the Serialize function from the streams package.
|
|
||||||
// However, this can be overridden after the side effect actor is intantiated,
|
|
||||||
// by setting the exposed Serialize function on the struct. For example:
|
|
||||||
//
|
|
||||||
// a := NewSideEffectActor(...)
|
|
||||||
// a.Serialize = func(a vocab.Type) (m map[string]interface{}, e error) {
|
|
||||||
// // Put your custom serializer logic here.
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// Note that you should only do this *immediately* after instantiating the side
|
|
||||||
// effect actor -- never while your application is already running, as this will
|
|
||||||
// likely cause race conditions or other problems! In most cases, you will never
|
|
||||||
// need to change this; it's provided solely to allow easier customization by
|
|
||||||
// applications.
|
|
||||||
type SideEffectActor struct {
|
type SideEffectActor struct {
|
||||||
|
// When doing deliveries to remote servers via the s2s protocol, the side effect
|
||||||
|
// actor will by default use the Serialize function from the streams package.
|
||||||
|
// However, this can be overridden after the side effect actor is intantiated,
|
||||||
|
// by setting the exposed Serialize function on the struct. For example:
|
||||||
|
//
|
||||||
|
// a := NewSideEffectActor(...)
|
||||||
|
// a.Serialize = func(a vocab.Type) (m map[string]interface{}, e error) {
|
||||||
|
// // Put your custom serializer logic here.
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that you should only do this *immediately* after instantiating the side
|
||||||
|
// effect actor -- never while your application is already running, as this will
|
||||||
|
// likely cause race conditions or other problems! In most cases, you will never
|
||||||
|
// need to change this; it's provided solely to allow easier customization by
|
||||||
|
// applications.
|
||||||
Serialize func(a vocab.Type) (m map[string]interface{}, e error)
|
Serialize func(a vocab.Type) (m map[string]interface{}, e error)
|
||||||
|
|
||||||
|
// When doing deliveries to remote servers via the s2s protocol, it may be desirable
|
||||||
|
// for implementations to be able to pre-sort recipients so that higher-priority
|
||||||
|
// recipients are higher up in the delivery queue, and lower-priority recipients
|
||||||
|
// are further down. This can be achieved by setting the DeliveryRecipientPreSort
|
||||||
|
// function on the side effect actor after it's instantiated. For example:
|
||||||
|
//
|
||||||
|
// a := NewSideEffectActor(...)
|
||||||
|
// a.DeliveryRecipientPreSort = func(actorAndCollectionIRIs []*url.URL) []*url.URL {
|
||||||
|
// // Put your sorting logic here.
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The actorAndCollectionIRIs parameter will be the initial list of IRIs derived by
|
||||||
|
// looking at the "to", "cc", "bto", "bcc", and "audience" properties of the activity
|
||||||
|
// being delivered, excluding the AP public IRI, and before dereferencing of inboxes.
|
||||||
|
// It may look something like this:
|
||||||
|
//
|
||||||
|
// [
|
||||||
|
// "https://example.org/users/someone/followers", // <-- collection IRI
|
||||||
|
// "https://another.example.org/users/someone_else", // <-- actor IRI
|
||||||
|
// "[...]" // <-- etc
|
||||||
|
// ]
|
||||||
|
//
|
||||||
|
// In this case, implementers may wish to sort the slice so that the directly-addressed
|
||||||
|
// actor "https://another.example.org/users/someone_else" occurs at an earlier index in
|
||||||
|
// the slice than the followers collection "https://example.org/users/someone/followers",
|
||||||
|
// so that "@someone_else" receives the delivery first.
|
||||||
|
//
|
||||||
|
// Note that you should only do this *immediately* after instantiating the side
|
||||||
|
// effect actor -- never while your application is already running, as this will
|
||||||
|
// likely cause race conditions or other problems! It's also completely fine to not
|
||||||
|
// set this function at all -- in this case, no pre-sorting of recipients will be
|
||||||
|
// performed, and delivery will occur in a non-determinate order.
|
||||||
|
DeliveryRecipientPreSort func(actorAndCollectionIRIs []*url.URL) []*url.URL
|
||||||
|
|
||||||
common CommonBehavior
|
common CommonBehavior
|
||||||
s2s FederatingProtocol
|
s2s FederatingProtocol
|
||||||
c2s SocialProtocol
|
c2s SocialProtocol
|
||||||
|
@ -652,195 +685,315 @@ func (a *SideEffectActor) hasInboxForwardingValues(c context.Context, inboxIRI *
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare takes a deliverableObject and returns a list of the proper recipient
|
// prepare takes a deliverableObject and returns a list of the final
|
||||||
// target URIs. Additionally, the deliverableObject will have any hidden
|
// recipient inbox IRIs. Additionally, the deliverableObject will have
|
||||||
// hidden recipients ("bto" and "bcc") stripped from it.
|
// any hidden hidden recipients ("bto" and "bcc") stripped from it.
|
||||||
//
|
//
|
||||||
// Only call if both the social and federated protocol are supported.
|
// Only call if both the social and federated protocol are supported.
|
||||||
func (a *SideEffectActor) prepare(c context.Context, outboxIRI *url.URL, activity Activity) (r []*url.URL, err error) {
|
func (a *SideEffectActor) prepare(
|
||||||
// Get inboxes of recipients
|
ctx context.Context,
|
||||||
|
outboxIRI *url.URL,
|
||||||
|
activity Activity,
|
||||||
|
) ([]*url.URL, error) {
|
||||||
|
// Iterate through to, bto, cc, bcc, and audience
|
||||||
|
// to extract a slice of addressee IRIs / IDs.
|
||||||
|
//
|
||||||
|
// The resulting slice might look something like:
|
||||||
|
//
|
||||||
|
// [
|
||||||
|
// "https://example.org/users/someone/followers", // <-- collection IRI
|
||||||
|
// "https://another.example.org/users/someone_else", // <-- actor IRI
|
||||||
|
// "[...]" // <-- etc
|
||||||
|
// ]
|
||||||
|
var actorsAndCollections []*url.URL
|
||||||
if to := activity.GetActivityStreamsTo(); to != nil {
|
if to := activity.GetActivityStreamsTo(); to != nil {
|
||||||
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
|
for iter := to.Begin(); iter != to.End(); iter = iter.Next() {
|
||||||
var val *url.URL
|
var err error
|
||||||
val, err = ToId(iter)
|
actorsAndCollections, err = appendToActorsAndCollectionsIRIs(
|
||||||
|
iter, actorsAndCollections,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
r = append(r, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bto := activity.GetActivityStreamsBto(); bto != nil {
|
if bto := activity.GetActivityStreamsBto(); bto != nil {
|
||||||
for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() {
|
for iter := bto.Begin(); iter != bto.End(); iter = iter.Next() {
|
||||||
var val *url.URL
|
var err error
|
||||||
val, err = ToId(iter)
|
actorsAndCollections, err = appendToActorsAndCollectionsIRIs(
|
||||||
|
iter, actorsAndCollections,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
r = append(r, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cc := activity.GetActivityStreamsCc(); cc != nil {
|
if cc := activity.GetActivityStreamsCc(); cc != nil {
|
||||||
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
|
for iter := cc.Begin(); iter != cc.End(); iter = iter.Next() {
|
||||||
var val *url.URL
|
var err error
|
||||||
val, err = ToId(iter)
|
actorsAndCollections, err = appendToActorsAndCollectionsIRIs(
|
||||||
|
iter, actorsAndCollections,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
r = append(r, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bcc := activity.GetActivityStreamsBcc(); bcc != nil {
|
if bcc := activity.GetActivityStreamsBcc(); bcc != nil {
|
||||||
for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() {
|
for iter := bcc.Begin(); iter != bcc.End(); iter = iter.Next() {
|
||||||
var val *url.URL
|
var err error
|
||||||
val, err = ToId(iter)
|
actorsAndCollections, err = appendToActorsAndCollectionsIRIs(
|
||||||
|
iter, actorsAndCollections,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
r = append(r, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if audience := activity.GetActivityStreamsAudience(); audience != nil {
|
if audience := activity.GetActivityStreamsAudience(); audience != nil {
|
||||||
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
|
for iter := audience.Begin(); iter != audience.End(); iter = iter.Next() {
|
||||||
var val *url.URL
|
var err error
|
||||||
val, err = ToId(iter)
|
actorsAndCollections, err = appendToActorsAndCollectionsIRIs(
|
||||||
|
iter, actorsAndCollections,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
r = append(r, val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 1. When an object is being delivered to the originating actor's
|
|
||||||
// followers, a server MAY reduce the number of receiving actors
|
|
||||||
// delivered to by identifying all followers which share the same
|
|
||||||
// sharedInbox who would otherwise be individual recipients and
|
|
||||||
// instead deliver objects to said sharedInbox.
|
|
||||||
// 2. If an object is addressed to the Public special collection, a
|
|
||||||
// server MAY deliver that object to all known sharedInbox endpoints
|
|
||||||
// on the network.
|
|
||||||
r = filterURLs(r, IsPublic)
|
|
||||||
|
|
||||||
// first check if the implemented database logic can return any inboxes
|
// PRE-SORTING
|
||||||
// from our list of actor IRIs.
|
|
||||||
foundInboxesFromDB := []*url.URL{}
|
// If the pre-delivery sort function is defined,
|
||||||
for _, actorIRI := range r {
|
// call it now so that implementations can sort
|
||||||
// BEGIN LOCK
|
// delivery order to their preferences.
|
||||||
var unlock func()
|
if a.DeliveryRecipientPreSort != nil {
|
||||||
unlock, err = a.db.Lock(c, actorIRI)
|
actorsAndCollections = a.DeliveryRecipientPreSort(actorsAndCollections)
|
||||||
if err != nil {
|
}
|
||||||
return
|
|
||||||
|
// We now need to dereference the actor or collection
|
||||||
|
// IRIs to derive inboxes that we can POST requests to.
|
||||||
|
var (
|
||||||
|
inboxes = make([]*url.URL, 0, len(actorsAndCollections))
|
||||||
|
derefdEntries = make(map[string]struct{}, len(actorsAndCollections))
|
||||||
|
)
|
||||||
|
|
||||||
|
// First check if the implemented database logic
|
||||||
|
// can return any of these inboxes without having
|
||||||
|
// to make remote dereference calls (much cheaper).
|
||||||
|
for _, actorOrCollection := range actorsAndCollections {
|
||||||
|
actorOrCollectionStr := actorOrCollection.String()
|
||||||
|
if _, derefd := derefdEntries[actorOrCollectionStr]; derefd {
|
||||||
|
// Ignore potential duplicates
|
||||||
|
// we've already derefd to inbox(es).
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
inboxes, err := a.db.InboxesForIRI(c, actorIRI)
|
// BEGIN LOCK
|
||||||
|
unlock, err := a.db.Lock(ctx, actorOrCollection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// bail on error
|
|
||||||
unlock()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(inboxes) > 0 {
|
// Try to get inbox(es) for this actor or collection.
|
||||||
// we have a hit
|
gotInboxes, err := a.db.InboxesForIRI(ctx, actorOrCollection)
|
||||||
foundInboxesFromDB = append(foundInboxesFromDB, inboxes...)
|
|
||||||
|
|
||||||
// if we found inboxes for this iri, we should remove it from
|
|
||||||
// the list of actors/iris we still need to dereference
|
|
||||||
r = removeOne(r, actorIRI)
|
|
||||||
}
|
|
||||||
|
|
||||||
// END LOCK
|
// END LOCK
|
||||||
unlock()
|
unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(gotInboxes) == 0 {
|
||||||
|
// No hit(s).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have one or more hits.
|
||||||
|
inboxes = append(inboxes, gotInboxes...)
|
||||||
|
|
||||||
|
// Mark this actor or collection as deref'd.
|
||||||
|
derefdEntries[actorOrCollectionStr] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for any actors' inboxes that weren't already discovered above;
|
// Now look for any remaining actors/collections
|
||||||
// find these by making dereference calls to remote instances
|
// that weren't already dereferenced into inboxes
|
||||||
t, err := a.common.NewTransport(c, outboxIRI, goFedUserAgent())
|
// with db calls; find these by making deref calls
|
||||||
if err != nil {
|
// to remote instances.
|
||||||
return nil, err
|
//
|
||||||
}
|
// First get a transport to do the http calls.
|
||||||
foundActorsFromRemote, err := a.resolveActors(c, t, r, 0, a.s2s.MaxDeliveryRecursionDepth(c))
|
t, err := a.common.NewTransport(ctx, outboxIRI, goFedUserAgent())
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
foundInboxesFromRemote, err := getInboxes(foundActorsFromRemote)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// combine this list of dereferenced inbox IRIs with the inboxes we already
|
// Make HTTP calls to unpack collection IRIs into
|
||||||
// found in the db, to make a complete list of target IRIs
|
// Actor IRIs and then into Actor types, ignoring
|
||||||
targets := []*url.URL{}
|
// actors or collections we've already deref'd.
|
||||||
targets = append(targets, foundInboxesFromDB...)
|
actorsFromRemote, err := a.resolveActors(
|
||||||
targets = append(targets, foundInboxesFromRemote...)
|
ctx,
|
||||||
|
t,
|
||||||
// Get inboxes of sender.
|
actorsAndCollections,
|
||||||
var unlock func()
|
derefdEntries,
|
||||||
unlock, err = a.db.Lock(c, outboxIRI)
|
0, a.s2s.MaxDeliveryRecursionDepth(ctx),
|
||||||
if err != nil {
|
)
|
||||||
return
|
|
||||||
}
|
|
||||||
// WARNING: No deferring the Unlock
|
|
||||||
actorIRI, err := a.db.ActorForOutbox(c, outboxIRI)
|
|
||||||
unlock() // unlock after regardless
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Get the inbox on the sender.
|
|
||||||
unlock, err = a.db.Lock(c, actorIRI)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Release no-longer-needed collections.
|
||||||
|
clear(derefdEntries)
|
||||||
|
clear(actorsAndCollections)
|
||||||
|
|
||||||
|
// Extract inbox IRI from each deref'd Actor (if any).
|
||||||
|
inboxesFromRemote, err := actorsToInboxIRIs(actorsFromRemote)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine db-discovered inboxes and remote-discovered
|
||||||
|
// inboxes into a final list of destination inboxes.
|
||||||
|
inboxes = append(inboxes, inboxesFromRemote...)
|
||||||
|
|
||||||
|
// POST FILTERING
|
||||||
|
|
||||||
|
// Do a final pass of the inboxes to:
|
||||||
|
//
|
||||||
|
// 1. Deduplicate entries.
|
||||||
|
// 2. Ensure that the list of inboxes doesn't
|
||||||
|
// contain the inbox of whoever the outbox
|
||||||
|
// belongs to, no point delivering to oneself.
|
||||||
|
//
|
||||||
|
// To do this we first need to get the
|
||||||
|
// inbox IRI of this outbox's Actor.
|
||||||
|
|
||||||
// BEGIN LOCK
|
// BEGIN LOCK
|
||||||
thisActor, err := a.db.Get(c, actorIRI)
|
unlock, err := a.db.Lock(ctx, outboxIRI)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the IRI of the Actor who owns this outbox.
|
||||||
|
outboxActorIRI, err := a.db.ActorForOutbox(ctx, outboxIRI)
|
||||||
|
|
||||||
|
// END LOCK
|
||||||
unlock()
|
unlock()
|
||||||
// END LOCK -- Still need to handle err
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Post-processing
|
|
||||||
var ignore *url.URL
|
// BEGIN LOCK
|
||||||
ignore, err = getInbox(thisActor)
|
unlock, err = a.db.Lock(ctx, outboxActorIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r = dedupeIRIs(targets, []*url.URL{ignore})
|
|
||||||
|
// Now get the Actor who owns this outbox.
|
||||||
|
outboxActor, err := a.db.Get(ctx, outboxActorIRI)
|
||||||
|
|
||||||
|
// END LOCK
|
||||||
|
unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the inbox IRI for the outbox Actor.
|
||||||
|
inboxOfOutboxActor, err := getInbox(outboxActor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate the final inboxes slice, and filter
|
||||||
|
// out of the inbox of this outbox actor (if present).
|
||||||
|
inboxes = filterInboxIRIs(inboxes, inboxOfOutboxActor)
|
||||||
|
|
||||||
|
// Now that we've derived inboxes to deliver
|
||||||
|
// the activity to, strip off any bto or bcc
|
||||||
|
// recipients, as per the AP spec requirements.
|
||||||
stripHiddenRecipients(activity)
|
stripHiddenRecipients(activity)
|
||||||
return r, nil
|
|
||||||
|
// All done!
|
||||||
|
return inboxes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveActors takes a list of Actor id URIs and returns them as concrete
|
// resolveActors takes a list of Actor id URIs and returns them as concrete
|
||||||
// instances of actorObject. It attempts to apply recursively when it encounters
|
// instances of actorObject. It attempts to apply recursively when it encounters
|
||||||
// a target that is a Collection or OrderedCollection.
|
// a target that is a Collection or OrderedCollection.
|
||||||
//
|
//
|
||||||
|
// Any IRI strings in the ignores map will be skipped (use this when
|
||||||
|
// you've already dereferenced some of the actorAndCollectionIRIs).
|
||||||
|
//
|
||||||
// If maxDepth is zero or negative, then recursion is infinitely applied.
|
// If maxDepth is zero or negative, then recursion is infinitely applied.
|
||||||
//
|
//
|
||||||
// If a recipient is a Collection or OrderedCollection, then the server MUST
|
// If a recipient is a Collection or OrderedCollection, then the server MUST
|
||||||
// dereference the collection, WITH the user's credentials.
|
// dereference the collection, WITH the user's credentials.
|
||||||
//
|
//
|
||||||
// Note that this also applies to CollectionPage and OrderedCollectionPage.
|
// Note that this also applies to CollectionPage and OrderedCollectionPage.
|
||||||
func (a *SideEffectActor) resolveActors(c context.Context, t Transport, r []*url.URL, depth, maxDepth int) (actors []vocab.Type, err error) {
|
func (a *SideEffectActor) resolveActors(
|
||||||
|
ctx context.Context,
|
||||||
|
t Transport,
|
||||||
|
actorAndCollectionIRIs []*url.URL,
|
||||||
|
ignores map[string]struct{},
|
||||||
|
depth, maxDepth int,
|
||||||
|
) ([]vocab.Type, error) {
|
||||||
if maxDepth > 0 && depth >= maxDepth {
|
if maxDepth > 0 && depth >= maxDepth {
|
||||||
return
|
// Hit our max depth.
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
for _, u := range r {
|
|
||||||
var act vocab.Type
|
if len(actorAndCollectionIRIs) == 0 {
|
||||||
var more []*url.URL
|
// Nothing to do.
|
||||||
// TODO: Determine if more logic is needed here for inaccessible
|
return nil, nil
|
||||||
// collections owned by peer servers.
|
}
|
||||||
act, more, err = a.dereferenceForResolvingInboxes(c, t, u)
|
|
||||||
|
// Optimistically assume 1:1 mapping of IRIs to actors.
|
||||||
|
actors := make([]vocab.Type, 0, len(actorAndCollectionIRIs))
|
||||||
|
|
||||||
|
// Deref each actorOrCollectionIRI if not ignored.
|
||||||
|
for _, actorOrCollectionIRI := range actorAndCollectionIRIs {
|
||||||
|
_, ignore := ignores[actorOrCollectionIRI.String()]
|
||||||
|
if ignore {
|
||||||
|
// Don't try to
|
||||||
|
// deref this one.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Determine if more logic is needed here for
|
||||||
|
// inaccessible collections owned by peer servers.
|
||||||
|
actor, more, err := a.dereferenceForResolvingInboxes(ctx, t, actorOrCollectionIRI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Missing recipient -- skip.
|
// Missing recipient -- skip.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var recurActors []vocab.Type
|
|
||||||
recurActors, err = a.resolveActors(c, t, more, depth+1, maxDepth)
|
if actor != nil {
|
||||||
|
// Got a hit.
|
||||||
|
actors = append(actors, actor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this was a collection, get more.
|
||||||
|
recurActors, err := a.resolveActors(
|
||||||
|
ctx,
|
||||||
|
t,
|
||||||
|
more,
|
||||||
|
ignores,
|
||||||
|
depth+1, maxDepth,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
|
||||||
if act != nil {
|
|
||||||
actors = append(actors, act)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
actors = append(actors, recurActors...)
|
actors = append(actors, recurActors...)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
|
return actors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dereferenceForResolvingInboxes dereferences an IRI solely for finding an
|
// dereferenceForResolvingInboxes dereferences an IRI solely for finding an
|
||||||
|
|
86
vendor/github.com/superseriousbusiness/activity/pub/util.go
generated
vendored
86
vendor/github.com/superseriousbusiness/activity/pub/util.go
generated
vendored
|
@ -385,19 +385,6 @@ func wrapInCreate(ctx context.Context, o vocab.Type, actor *url.URL) (c vocab.Ac
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterURLs removes urls whose strings match the provided filter
|
|
||||||
func filterURLs(u []*url.URL, fn func(s string) bool) []*url.URL {
|
|
||||||
i := 0
|
|
||||||
for i < len(u) {
|
|
||||||
if fn(u[i].String()) {
|
|
||||||
u = append(u[:i], u[i+1:]...)
|
|
||||||
} else {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return u
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PublicActivityPubIRI is the IRI that indicates an Activity is meant
|
// PublicActivityPubIRI is the IRI that indicates an Activity is meant
|
||||||
// to be visible for general public consumption.
|
// to be visible for general public consumption.
|
||||||
|
@ -412,8 +399,28 @@ func IsPublic(s string) bool {
|
||||||
return s == PublicActivityPubIRI || s == publicJsonLD || s == publicJsonLDAS
|
return s == PublicActivityPubIRI || s == publicJsonLD || s == publicJsonLDAS
|
||||||
}
|
}
|
||||||
|
|
||||||
// getInboxes extracts the 'inbox' IRIs from actor types.
|
// Derives an ID URI from the given IdProperty and, if it's not the
|
||||||
func getInboxes(t []vocab.Type) (u []*url.URL, err error) {
|
// magic AP Public IRI, appends it to the actorsAndCollections slice.
|
||||||
|
func appendToActorsAndCollectionsIRIs(
|
||||||
|
iter IdProperty,
|
||||||
|
actorsAndCollections []*url.URL,
|
||||||
|
) ([]*url.URL, error) {
|
||||||
|
id, err := ToId(iter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore Public IRI as we
|
||||||
|
// can't deliver to it directly.
|
||||||
|
if !IsPublic(id.String()) {
|
||||||
|
actorsAndCollections = append(actorsAndCollections, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return actorsAndCollections, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// actorsToInboxIRIs extracts the 'inbox' IRIs from actor types.
|
||||||
|
func actorsToInboxIRIs(t []vocab.Type) (u []*url.URL, err error) {
|
||||||
for _, elem := range t {
|
for _, elem := range t {
|
||||||
var iri *url.URL
|
var iri *url.URL
|
||||||
iri, err = getInbox(elem)
|
iri, err = getInbox(elem)
|
||||||
|
@ -436,32 +443,37 @@ func getInbox(t vocab.Type) (u *url.URL, err error) {
|
||||||
return ToId(inbox)
|
return ToId(inbox)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dedupeIRIs will deduplicate final inbox IRIs. The ignore list is applied to
|
// filterInboxIRIs will deduplicate the given inboxes
|
||||||
// the final list.
|
// slice, while also leaving out any filtered IRIs.
|
||||||
func dedupeIRIs(recipients, ignored []*url.URL) (out []*url.URL) {
|
func filterInboxIRIs(
|
||||||
ignoredMap := make(map[string]bool, len(ignored))
|
inboxes []*url.URL,
|
||||||
for _, elem := range ignored {
|
filtered ...*url.URL,
|
||||||
ignoredMap[elem.String()] = true
|
) []*url.URL {
|
||||||
|
// Prepopulate the ignored map with each filtered IRI.
|
||||||
|
ignored := make(map[string]struct{}, len(filtered)+len(inboxes))
|
||||||
|
for _, filteredIRI := range filtered {
|
||||||
|
ignored[filteredIRI.String()] = struct{}{}
|
||||||
}
|
}
|
||||||
outMap := make(map[string]bool, len(recipients))
|
|
||||||
for _, k := range recipients {
|
|
||||||
kStr := k.String()
|
|
||||||
if !ignoredMap[kStr] && !outMap[kStr] {
|
|
||||||
out = append(out, k)
|
|
||||||
outMap[kStr] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// removeOne removes any occurrences of entry from a slice of entries.
|
deduped := make([]*url.URL, 0, len(inboxes))
|
||||||
func removeOne(entries []*url.URL, entry *url.URL) (out []*url.URL) {
|
for _, inbox := range inboxes {
|
||||||
for _, e := range entries {
|
inboxStr := inbox.String()
|
||||||
if e.String() != entry.String() {
|
_, ignore := ignored[inboxStr]
|
||||||
out = append(out, e)
|
if ignore {
|
||||||
|
// We already included
|
||||||
|
// this URI in out, or
|
||||||
|
// we should ignore it.
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include this IRI in output, and
|
||||||
|
// add entry to the ignored map to
|
||||||
|
// ensure we don't include it again.
|
||||||
|
deduped = append(deduped, inbox)
|
||||||
|
ignored[inboxStr] = struct{}{}
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
|
return deduped
|
||||||
}
|
}
|
||||||
|
|
||||||
// stripHiddenRecipients removes "bto" and "bcc" from the activity.
|
// stripHiddenRecipients removes "bto" and "bcc" from the activity.
|
||||||
|
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
|
@ -660,7 +660,7 @@ github.com/stretchr/testify/suite
|
||||||
# github.com/subosito/gotenv v1.6.0
|
# github.com/subosito/gotenv v1.6.0
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/subosito/gotenv
|
github.com/subosito/gotenv
|
||||||
# github.com/superseriousbusiness/activity v1.9.0-gts
|
# github.com/superseriousbusiness/activity v1.10.0-gts
|
||||||
## explicit; go 1.21
|
## explicit; go 1.21
|
||||||
github.com/superseriousbusiness/activity/pub
|
github.com/superseriousbusiness/activity/pub
|
||||||
github.com/superseriousbusiness/activity/streams
|
github.com/superseriousbusiness/activity/streams
|
||||||
|
|
Loading…
Reference in a new issue