mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 02:02:25 -05:00 
			
		
		
		
	[bugfix] Fix incorrect json-ld @context serialization (#3243)
		
	This commit is contained in:
		
					parent
					
						
							
								f4d69db36a
							
						
					
				
			
			
				commit
				
					
						8a34e4c28f
					
				
			
		
					 6 changed files with 325 additions and 80 deletions
				
			
		
							
								
								
									
										218
									
								
								vendor/github.com/superseriousbusiness/activity/streams/util.go
									
										
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										218
									
								
								vendor/github.com/superseriousbusiness/activity/streams/util.go
									
										
									
										generated
									
									
										vendored
									
									
								
							|  | @ -1,6 +1,9 @@ | |||
| package streams | ||||
| 
 | ||||
| import ( | ||||
| 	"maps" | ||||
| 	"slices" | ||||
| 
 | ||||
| 	"github.com/superseriousbusiness/activity/streams/vocab" | ||||
| ) | ||||
| 
 | ||||
|  | @ -10,47 +13,200 @@ const ( | |||
| 	// rest of the payload. Important for linked-data representations, but | ||||
| 	// only applicable to go-fed at code-generation time. | ||||
| 	jsonLDContext = "@context" | ||||
| 
 | ||||
| 	asNS     = "https://www.w3.org/ns/activitystreams" | ||||
| 	tootNS   = "http://joinmastodon.org/ns" | ||||
| 	schemaNS = "http://schema.org" | ||||
| ) | ||||
| 
 | ||||
| // Map of inlines @context entries that may need to be added | ||||
| // when vocabs include "https://www.w3.org/ns/activitystreams". | ||||
| var asInlines = map[string]any{ | ||||
| 	"Hashtag":                   "as:Hashtag", | ||||
| 	"alsoKnownAs":               "as:alsoKnownAs", | ||||
| 	"manuallyApprovesFollowers": "as:manuallyApprovesFollowers", | ||||
| 	"sensitive":                 "as:sensitive", | ||||
| 
 | ||||
| 	"movedTo": map[string]string{ | ||||
| 		"@id":   "as:movedTo", | ||||
| 		"@type": "@id", | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| // Map of inlines @context entries that may need to be | ||||
| // added when vocabs include "http://joinmastodon.org/ns". | ||||
| var tootInlines = map[string]any{ | ||||
| 	"Emoji":        "toot:Emoji", | ||||
| 	"blurhash":     "toot:blurhash", | ||||
| 	"discoverable": "toot:discoverable", | ||||
| 	"indexable":    "toot:indexable", | ||||
| 	"memorial":     "toot:memorial", | ||||
| 	"suspended":    "toot:suspended", | ||||
| 	"votersCount":  "toot:votersCount", | ||||
| 
 | ||||
| 	"featured": map[string]string{ | ||||
| 		"@id":   "toot:featured", | ||||
| 		"@type": "@id", | ||||
| 	}, | ||||
| 
 | ||||
| 	"featuredTags": map[string]string{ | ||||
| 		"@id":   "toot:featuredTags", | ||||
| 		"@type": "@id", | ||||
| 	}, | ||||
| 
 | ||||
| 	"focalPoint": map[string]string{ | ||||
| 		"@container": "@list", | ||||
| 		"@id":        "toot:focalPoint", | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| // Map of inlines @context entries that may need to | ||||
| // be added when vocabs include "http://schema.org". | ||||
| var schemaInlines = map[string]any{ | ||||
| 	"PropertyValue": "schema:PropertyValue", | ||||
| 	"value":         "schema:value", | ||||
| } | ||||
| 
 | ||||
| // getLookup returns a lookup map of all interesting field names | ||||
| // + type names on the given "in" map that may need to be inlined. | ||||
| func getLookup(in map[string]any) map[string]struct{} { | ||||
| 	out := make(map[string]struct{}) | ||||
| 
 | ||||
| 	for k, v := range in { | ||||
| 		// Pull out keys from any nested maps. | ||||
| 		if nested, ok := v.(map[string]any); ok { | ||||
| 			maps.Copy(out, getLookup(nested)) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		// Pull out keys from any | ||||
| 		// arrays of nested maps. | ||||
| 		if nestedIs, ok := v.([]any); ok { | ||||
| 			for _, nestedI := range nestedIs { | ||||
| 				if nested, ok := nestedI.(map[string]any); ok { | ||||
| 					maps.Copy(out, getLookup(nested)) | ||||
| 					continue | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// For types, we actually care about | ||||
| 		// the *value*, ie., the name of the | ||||
| 		// type, not the type key itself. | ||||
| 		if k == "type" { | ||||
| 			out[v.(string)] = struct{}{} | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		out[k] = struct{}{} | ||||
| 	} | ||||
| 
 | ||||
| 	return out | ||||
| } | ||||
| 
 | ||||
| func copyInlines( | ||||
| 	src map[string]any, | ||||
| 	dst map[string]any, | ||||
| 	lookup map[string]struct{}, | ||||
| ) { | ||||
| 	for k, v := range src { | ||||
| 		_, ok := lookup[k] | ||||
| 		if ok { | ||||
| 			dst[k] = v | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Serialize adds the context vocabularies contained within the type | ||||
| // into the JSON-LD @context field, and aliases them appropriately. | ||||
| func Serialize(a vocab.Type) (m map[string]interface{}, e error) { | ||||
| func Serialize(a vocab.Type) (m map[string]any, e error) { | ||||
| 	m, e = a.Serialize() | ||||
| 	if e != nil { | ||||
| 		return | ||||
| 	} | ||||
| 	v := a.JSONLDContext() | ||||
| 	// Transform the map of vocabulary-to-aliases into a context payload, | ||||
| 	// but do so in a way that at least keeps it readable for other humans. | ||||
| 	var contextValue interface{} | ||||
| 	if len(v) == 1 { | ||||
| 		for vocab, alias := range v { | ||||
| 			if len(alias) == 0 { | ||||
| 				contextValue = vocab | ||||
| 			} else { | ||||
| 				contextValue = map[string]string{ | ||||
| 					alias: vocab, | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 	var ( | ||||
| 		// Slice of vocab URIs | ||||
| 		// used in this vocab.Type. | ||||
| 		vocabs = a.JSONLDContext() | ||||
| 
 | ||||
| 		// Slice of vocab URIs to add | ||||
| 		// to the base @context slice. | ||||
| 		includeVocabs []string | ||||
| 
 | ||||
| 		// Object to inline as an extra | ||||
| 		// entry in the @context slice. | ||||
| 		inlinedContext = make(map[string]any) | ||||
| 	) | ||||
| 
 | ||||
| 	// Get a lookup of all field and | ||||
| 	// type names we need to care about. | ||||
| 	lookup := getLookup(m) | ||||
| 
 | ||||
| 	// Go through each used vocab and see | ||||
| 	// if we need to special case it. | ||||
| 	for vocab := range vocabs { | ||||
| 
 | ||||
| 		switch vocab { | ||||
| 
 | ||||
| 		case asNS: | ||||
| 			// ActivityStreams vocab. | ||||
| 			// | ||||
| 			// The namespace URI already points to | ||||
| 			// a proper @context document but we | ||||
| 			// need to add some extra inlines. | ||||
| 			includeVocabs = append(includeVocabs, asNS) | ||||
| 			copyInlines(asInlines, inlinedContext, lookup) | ||||
| 
 | ||||
| 		case schemaNS: | ||||
| 			// Schema vocab. | ||||
| 			// | ||||
| 			// The URI doesn't point to a @context | ||||
| 			// document so we need to inline everything. | ||||
| 			inlinedContext["schema"] = schemaNS + "#" | ||||
| 			copyInlines(schemaInlines, inlinedContext, lookup) | ||||
| 
 | ||||
| 		case tootNS: | ||||
| 			// Toot/Mastodon vocab. | ||||
| 			// | ||||
| 			// The URI doesn't point to a @context | ||||
| 			// document so we need to inline everything. | ||||
| 			inlinedContext["toot"] = tootNS + "#" | ||||
| 			copyInlines(tootInlines, inlinedContext, lookup) | ||||
| 
 | ||||
| 		default: | ||||
| 			// No special case. | ||||
| 			includeVocabs = append(includeVocabs, vocab) | ||||
| 		} | ||||
| 	} else { | ||||
| 		var arr []interface{} | ||||
| 		aliases := make(map[string]string) | ||||
| 		for vocab, alias := range v { | ||||
| 			if len(alias) == 0 { | ||||
| 				arr = append(arr, vocab) | ||||
| 			} else { | ||||
| 				aliases[alias] = vocab | ||||
| 			} | ||||
| 		} | ||||
| 		if len(aliases) > 0 { | ||||
| 			arr = append(arr, aliases) | ||||
| 		} | ||||
| 		contextValue = arr | ||||
| 	} | ||||
| 	// TODO: Update the context instead if it already exists | ||||
| 	m[jsonLDContext] = contextValue | ||||
| 	// TODO: Sort the context based on arbitrary order. | ||||
| 
 | ||||
| 	// Sort used vocab entries alphabetically | ||||
| 	// to make their ordering predictable. | ||||
| 	slices.Sort(includeVocabs) | ||||
| 
 | ||||
| 	// Create final slice of @context | ||||
| 	// entries we'll need to include. | ||||
| 	contextEntries := make([]any, 0, len(includeVocabs)+1) | ||||
| 
 | ||||
| 	// Append each included vocab to the slice. | ||||
| 	for _, vocab := range includeVocabs { | ||||
| 		contextEntries = append(contextEntries, vocab) | ||||
| 	} | ||||
| 
 | ||||
| 	// Append any inlinedContext to the slice. | ||||
| 	if len(inlinedContext) != 0 { | ||||
| 		contextEntries = append(contextEntries, inlinedContext) | ||||
| 	} | ||||
| 
 | ||||
| 	// Include @context on the final output, | ||||
| 	// using an array if there's more than | ||||
| 	// one entry, just a property otherwise. | ||||
| 	if len(contextEntries) != 1 { | ||||
| 		m[jsonLDContext] = contextEntries | ||||
| 	} else { | ||||
| 		m[jsonLDContext] = contextEntries[0] | ||||
| 	} | ||||
| 
 | ||||
| 	// Delete any existing `@context` in child maps. | ||||
| 	var cleanFnRecur func(map[string]interface{}) | ||||
| 	cleanFnRecur = func(r map[string]interface{}) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue