2021-08-12 19:03:24 +00:00
|
|
|
package streams
|
|
|
|
|
|
|
|
import (
|
2024-08-26 17:17:45 +00:00
|
|
|
"maps"
|
|
|
|
"slices"
|
|
|
|
|
2021-11-13 16:29:43 +00:00
|
|
|
"github.com/superseriousbusiness/activity/streams/vocab"
|
2021-08-12 19:03:24 +00: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 17:17:45 +00:00
|
|
|
|
|
|
|
asNS = "https://www.w3.org/ns/activitystreams"
|
|
|
|
tootNS = "http://joinmastodon.org/ns"
|
|
|
|
schemaNS = "http://schema.org"
|
2021-08-12 19:03:24 +00:00
|
|
|
)
|
|
|
|
|
2024-08-26 17:17:45 +00: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",
|
|
|
|
"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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-12 19:03:24 +00:00
|
|
|
// Serialize adds the context vocabularies contained within the type
|
|
|
|
// into the JSON-LD @context field, and aliases them appropriately.
|
2024-08-26 17:17:45 +00:00
|
|
|
func Serialize(a vocab.Type) (m map[string]any, e error) {
|
2021-08-12 19:03:24 +00:00
|
|
|
m, e = a.Serialize()
|
|
|
|
if e != nil {
|
|
|
|
return
|
|
|
|
}
|
2024-08-26 17:17:45 +00: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 19:03:24 +00:00
|
|
|
}
|
2024-08-26 17:17:45 +00: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 19:03:24 +00:00
|
|
|
} else {
|
2024-08-26 17:17:45 +00:00
|
|
|
m[jsonLDContext] = contextEntries[0]
|
2021-08-12 19:03:24 +00:00
|
|
|
}
|
2024-08-26 17:17:45 +00:00
|
|
|
|
2021-08-12 19:03:24 +00: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
|
|
|
|
}
|