mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 14:32:24 -05: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
					
				
			
		
					 6 changed files with 386 additions and 165 deletions
				
			
		
							
								
								
									
										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 @@ import ( | ||||||
| 	"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 @@ import ( | ||||||
| 	"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, | ||||||
|  |  | ||||||
							
								
								
									
										381
									
								
								vendor/github.com/superseriousbusiness/activity/pub/side_effect_actor.go
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										381
									
								
								vendor/github.com/superseriousbusiness/activity/pub/side_effect_actor.go
									
										
									
										generated
									
									
										vendored
									
									
								
							|  | @ -20,7 +20,7 @@ var _ DelegateActor = &SideEffectActor{} | ||||||
| // 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. | ||||||
| // | type SideEffectActor struct { | ||||||
| 	// When doing deliveries to remote servers via the s2s protocol, the side effect | 	// 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. | 	// actor will by default use the Serialize function from the streams package. | ||||||
| 	// However, this can be overridden after the side effect actor is intantiated, | 	// However, this can be overridden after the side effect actor is intantiated, | ||||||
|  | @ -36,9 +36,42 @@ var _ DelegateActor = &SideEffectActor{} | ||||||
| 	// likely cause race conditions or other problems! In most cases, you will never | 	// 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 | 	// need to change this; it's provided solely to allow easier customization by | ||||||
| 	// applications. | 	// applications. | ||||||
| type SideEffectActor struct { |  | ||||||
| 	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 | ||||||
|  | 	// delivery order to their preferences. | ||||||
|  | 	if a.DeliveryRecipientPreSort != nil { | ||||||
|  | 		actorsAndCollections = a.DeliveryRecipientPreSort(actorsAndCollections) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// 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 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// BEGIN LOCK | 		// BEGIN LOCK | ||||||
| 		var unlock func() | 		unlock, err := a.db.Lock(ctx, actorOrCollection) | ||||||
| 		unlock, err = a.db.Lock(c, actorIRI) |  | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		inboxes, err := a.db.InboxesForIRI(c, actorIRI) |  | ||||||
| 		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() | ||||||
| 	} |  | ||||||
| 
 | 
 | ||||||
| 	// look for any actors' inboxes that weren't already discovered above; |  | ||||||
| 	// find these by making dereference calls to remote instances |  | ||||||
| 	t, err := a.common.NewTransport(c, outboxIRI, goFedUserAgent()) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 	foundActorsFromRemote, err := a.resolveActors(c, t, r, 0, a.s2s.MaxDeliveryRecursionDepth(c)) |  | ||||||
| 	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 | 		if len(gotInboxes) == 0 { | ||||||
| 	// found in the db, to make a complete list of target IRIs | 			// No hit(s). | ||||||
| 	targets := []*url.URL{} | 			continue | ||||||
| 	targets = append(targets, foundInboxesFromDB...) | 		} | ||||||
| 	targets = append(targets, foundInboxesFromRemote...) |  | ||||||
| 
 | 
 | ||||||
| 	// Get inboxes of sender. | 		// We have one or more hits. | ||||||
| 	var unlock func() | 		inboxes = append(inboxes, gotInboxes...) | ||||||
| 	unlock, err = a.db.Lock(c, outboxIRI) | 
 | ||||||
| 	if err != nil { | 		// Mark this actor or collection as deref'd. | ||||||
| 		return | 		derefdEntries[actorOrCollectionStr] = struct{}{} | ||||||
| 	} | 	} | ||||||
| 	// WARNING: No deferring the Unlock | 
 | ||||||
| 	actorIRI, err := a.db.ActorForOutbox(c, outboxIRI) | 	// Now look for any remaining actors/collections | ||||||
| 	unlock() // unlock after regardless | 	// that weren't already dereferenced into inboxes | ||||||
| 	if err != nil { | 	// with db calls; find these by making deref calls | ||||||
| 		return | 	// to remote instances. | ||||||
| 	} | 	// | ||||||
| 	// Get the inbox on the sender. | 	// First get a transport to do the http calls. | ||||||
| 	unlock, err = a.db.Lock(c, actorIRI) | 	t, err := a.common.NewTransport(ctx, outboxIRI, goFedUserAgent()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// Make HTTP calls to unpack collection IRIs into | ||||||
|  | 	// Actor IRIs and then into Actor types, ignoring | ||||||
|  | 	// actors or collections we've already deref'd. | ||||||
|  | 	actorsFromRemote, err := a.resolveActors( | ||||||
|  | 		ctx, | ||||||
|  | 		t, | ||||||
|  | 		actorsAndCollections, | ||||||
|  | 		derefdEntries, | ||||||
|  | 		0, a.s2s.MaxDeliveryRecursionDepth(ctx), | ||||||
|  | 	) | ||||||
|  | 	if err != nil { | ||||||
|  | 		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. | ||||||
| 	outMap := make(map[string]bool, len(recipients)) | 	ignored := make(map[string]struct{}, len(filtered)+len(inboxes)) | ||||||
| 	for _, k := range recipients { | 	for _, filteredIRI := range filtered { | ||||||
| 		kStr := k.String() | 		ignored[filteredIRI.String()] = struct{}{} | ||||||
| 		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…
	
	Add table
		Add a link
		
	
		Reference in a new issue