| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | package streams | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 	"maps" | 
					
						
							|  |  |  | 	"slices" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-25 15:15:36 +02:00
										 |  |  | 	"code.superseriousbusiness.org/activity/streams/vocab" | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const ( | 
					
						
							|  |  |  | 	// jsonLDContext is the key for the JSON-LD specification's context | 
					
						
							|  |  |  | 	// value. It contains the definitions of the types contained within the | 
					
						
							|  |  |  | 	// rest of the payload. Important for linked-data representations, but | 
					
						
							|  |  |  | 	// only applicable to go-fed at code-generation time. | 
					
						
							|  |  |  | 	jsonLDContext = "@context" | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-04-24 11:20:03 +02:00
										 |  |  | 	asNS     = "https://www.w3.org/ns/activitystreams" | 
					
						
							|  |  |  | 	tootNS   = "http://joinmastodon.org/ns" | 
					
						
							|  |  |  | 	schemaNS = "http://schema.org" | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | // 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", | 
					
						
							|  |  |  | 	"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 | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | // Serialize adds the context vocabularies contained within the type | 
					
						
							|  |  |  | // into the JSON-LD @context field, and aliases them appropriately. | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | func Serialize(a vocab.Type) (m map[string]any, e error) { | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | 	m, e = a.Serialize() | 
					
						
							|  |  |  | 	if e != nil { | 
					
						
							|  |  |  | 		return | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	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) | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// 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 | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | 	} else { | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 		m[jsonLDContext] = contextEntries[0] | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-08-26 19:17:45 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-12 21:03:24 +02:00
										 |  |  | 	// Delete any existing `@context` in child maps. | 
					
						
							|  |  |  | 	var cleanFnRecur func(map[string]interface{}) | 
					
						
							|  |  |  | 	cleanFnRecur = func(r map[string]interface{}) { | 
					
						
							|  |  |  | 		for _, v := range r { | 
					
						
							|  |  |  | 			if n, ok := v.(map[string]interface{}); ok { | 
					
						
							|  |  |  | 				delete(n, jsonLDContext) | 
					
						
							|  |  |  | 				cleanFnRecur(n) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	cleanFnRecur(m) | 
					
						
							|  |  |  | 	return | 
					
						
							|  |  |  | } |