diff options
author | 2023-11-21 15:13:30 +0100 | |
---|---|---|
committer | 2023-11-21 15:13:30 +0100 | |
commit | cfefbc08d822cd85787d95dc2ee253e3368826d8 (patch) | |
tree | af6d6257dddca1645ab5f8e34a1c79ac80d82e0e /internal/ap | |
parent | [docs] Annotate split-domain setup (#2372) (diff) | |
download | gotosocial-cfefbc08d822cd85787d95dc2ee253e3368826d8.tar.xz |
[feature] Federate status language in and out (#2366)
* [feature] Federate status language in + out
* go fmt
* tests, little fix
* improve comments
* unnest a bit
* avoid unnecessary nil check
* use more descriptive variable for contentMap
* prefer instance languages when selecting from contentMap
* update docs to reflect lang selection
* rename rdfLangString -> rdfLangs
* update comments to mention Pollable
* iter through slice instead of map
Diffstat (limited to 'internal/ap')
-rw-r--r-- | internal/ap/ap_test.go | 6 | ||||
-rw-r--r-- | internal/ap/extract.go | 37 | ||||
-rw-r--r-- | internal/ap/extractcontent_test.go | 5 | ||||
-rw-r--r-- | internal/ap/normalize.go | 285 | ||||
-rw-r--r-- | internal/ap/normalize_test.go | 82 | ||||
-rw-r--r-- | internal/ap/serialize.go | 139 |
6 files changed, 411 insertions, 143 deletions
diff --git a/internal/ap/ap_test.go b/internal/ap/ap_test.go index 6a5073c63..583a37c53 100644 --- a/internal/ap/ap_test.go +++ b/internal/ap/ap_test.go @@ -93,6 +93,12 @@ func noteWithMentions1() vocab.ActivityStreamsNote { content := streams.NewActivityStreamsContentProperty() content.AppendXMLSchemaString("hey @f0x and @dumpsterqueer") + + rdfLangString := make(map[string]string) + rdfLangString["en"] = "hey @f0x and @dumpsterqueer" + rdfLangString["fr"] = "bonjour @f0x et @dumpsterqueer" + content.AppendRDFLangString(rdfLangString) + note.SetActivityStreamsContent(content) return note diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 424f77409..3d92fa2ba 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -631,27 +631,34 @@ func ExtractPublicKey(i WithPublicKey) ( return nil, nil, nil, gtserror.New("couldn't find public key") } -// ExtractContent returns a string representation of the -// given interface's Content property, or an empty string -// if no Content is found. -func ExtractContent(i WithContent) string { - contentProperty := i.GetActivityStreamsContent() - if contentProperty == nil { - return "" +// ExtractContent returns an intermediary representation of +// the given interface's Content and/or ContentMap property. +func ExtractContent(i WithContent) gtsmodel.Content { + content := gtsmodel.Content{} + + contentProp := i.GetActivityStreamsContent() + if contentProp == nil { + // No content at all. + return content } - for iter := contentProperty.Begin(); iter != contentProperty.End(); iter = iter.Next() { + for iter := contentProp.Begin(); iter != contentProp.End(); iter = iter.Next() { switch { - // Content may be parsed as IRI, depending on - // how it's formatted, so account for this. - case iter.IsXMLSchemaString(): - return iter.GetXMLSchemaString() - case iter.IsIRI(): - return iter.GetIRI().String() + case iter.IsRDFLangString() && + len(content.ContentMap) == 0: + content.ContentMap = iter.GetRDFLangString() + + case iter.IsXMLSchemaString() && + content.Content == "": + content.Content = iter.GetXMLSchemaString() + + case iter.IsIRI() && + content.Content == "": + content.Content = iter.GetIRI().String() } } - return "" + return content } // ExtractAttachments attempts to extract barebones MediaAttachment objects from given AS interface type. diff --git a/internal/ap/extractcontent_test.go b/internal/ap/extractcontent_test.go index 590d1b931..c899a10e1 100644 --- a/internal/ap/extractcontent_test.go +++ b/internal/ap/extractcontent_test.go @@ -30,10 +30,11 @@ type ExtractContentTestSuite struct { func (suite *ExtractContentTestSuite) TestExtractContent1() { note := suite.noteWithMentions1 - content := ap.ExtractContent(note) - suite.Equal("hey @f0x and @dumpsterqueer", content) + suite.Equal("hey @f0x and @dumpsterqueer", content.Content) + suite.Equal("bonjour @f0x et @dumpsterqueer", content.ContentMap["fr"]) + suite.Equal("hey @f0x and @dumpsterqueer", content.ContentMap["en"]) } func TestExtractContentTestSuite(t *testing.T) { diff --git a/internal/ap/normalize.go b/internal/ap/normalize.go index 192a2d740..a27527b84 100644 --- a/internal/ap/normalize.go +++ b/internal/ap/normalize.go @@ -20,11 +20,12 @@ package ap import ( "github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/text" ) /* - NORMALIZE INCOMING + INCOMING NORMALIZATION The below functions should be called to normalize the content of messages *COMING INTO* GoToSocial via the federation API, either as the result of delivery from a remote instance to this @@ -84,39 +85,84 @@ func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interfa } } -// NormalizeIncomingContent replaces the Content of the given item -// with the sanitized version of the raw 'content' value from the -// raw json object map. +// normalizeContent normalizes the given content +// string by sanitizing its HTML and minimizing it. // -// noop if there was no content in the json object map or the -// content was not a plain string. -func NormalizeIncomingContent(item WithContent, rawJSON map[string]interface{}) { - rawContent, ok := rawJSON["content"] - if !ok { - // No content in rawJSON. - // TODO: In future we might also - // look for "contentMap" property. - return +// Noop for non-string content. +func normalizeContent(rawContent interface{}) string { + if rawContent == nil { + // Nothing to fix. + return "" } content, ok := rawContent.(string) if !ok { - // Not interested in content arrays. - return + // Not interested in + // content slices etc. + return "" + } + + if content == "" { + // Nothing to fix. + return "" } - // Content should be HTML encoded by default: + // Content entries should be HTML encoded by default: // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content // // TODO: sanitize differently based on mediaType. // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype content = text.SanitizeToHTML(content) content = text.MinifyHTML(content) + return content +} - // Set normalized content property from the raw string; - // this replaces any existing content property on the item. +// NormalizeIncomingContent replaces the Content property of the given +// item with the normalized versions of the raw 'content' and 'contentMap' +// values from the raw json object map. +// +// noop if there was no 'content' or 'contentMap' in the json object map. +func NormalizeIncomingContent(item WithContent, rawJSON map[string]interface{}) { + var ( + rawContent = rawJSON["content"] + rawContentMap = rawJSON["contentMap"] + ) + + if rawContent == nil && + rawContentMap == nil { + // Nothing to normalize, + // leave no content on item. + return + } + + // Create wrapper for normalized content. contentProp := streams.NewActivityStreamsContentProperty() - contentProp.AppendXMLSchemaString(content) + + // Fix 'content' if applicable. + content := normalizeContent(rawContent) + if content != "" { + contentProp.AppendXMLSchemaString(content) + } + + // Fix 'contentMap' if applicable. + contentMap, ok := rawContentMap.(map[string]interface{}) + if ok { + rdfLangs := make(map[string]string, len(contentMap)) + + for lang, rawContent := range contentMap { + content := normalizeContent(rawContent) + if content != "" { + rdfLangs[lang] = content + } + } + + if len(rdfLangs) != 0 { + contentProp.AppendRDFLangString(rdfLangs) + } + } + + // Replace any existing content property + // on the item with normalized version. item.SetActivityStreamsContent(contentProp) } @@ -299,3 +345,204 @@ func NormalizeIncomingPollOptions(item WithOneOf, rawJSON map[string]interface{} NormalizeIncomingName(choiceable, rawChoice) } } + +/* + OUTGOING NORMALIZATION + The below functions should be called to normalize the content + of messages *GOING OUT OF* GoToSocial via the federation API, + either as the result of delivery to a remote instance from this + instance, or as a result of a remote instance doing an http call + to us to dereference something. +*/ + +// NormalizeOutgoingAttachmentProp replaces single-entry Attachment objects with +// single-entry arrays, for better compatibility with other AP implementations. +// +// Ie: +// +// "attachment": { +// ... +// } +// +// becomes: +// +// "attachment": [ +// { +// ... +// } +// ] +// +// Noop for items with no attachments, or with attachments that are already a slice. +func NormalizeOutgoingAttachmentProp(item WithAttachment, rawJSON map[string]interface{}) { + attachment, ok := rawJSON["attachment"] + if !ok { + // No 'attachment', + // nothing to change. + return + } + + if _, ok := attachment.([]interface{}); ok { + // Already slice, + // nothing to change. + return + } + + // Coerce single-object to slice. + rawJSON["attachment"] = []interface{}{attachment} +} + +// NormalizeOutgoingContentProp normalizes go-fed's funky formatting of content and +// contentMap properties to a format better understood by other AP implementations. +// +// Ie., incoming "content" property like this: +// +// "content": [ +// "hello world!", +// { +// "en": "hello world!" +// } +// ] +// +// Is unpacked to: +// +// "content": "hello world!", +// "contentMap": { +// "en": "hello world!" +// } +// +// Noop if neither content nor contentMap are set. +func NormalizeOutgoingContentProp(item WithContent, rawJSON map[string]interface{}) { + contentProp := item.GetActivityStreamsContent() + if contentProp == nil { + // Nothing to do, + // bail early. + return + } + + contentPropLen := contentProp.Len() + if contentPropLen == 0 { + // Nothing to do, + // bail early. + return + } + + var ( + content string + contentMap map[string]string + ) + + for iter := contentProp.Begin(); iter != contentProp.End(); iter = iter.Next() { + switch { + case iter.IsRDFLangString() && + contentMap == nil: + contentMap = iter.GetRDFLangString() + + case content == "" && + iter.IsXMLSchemaString(): + content = iter.GetXMLSchemaString() + } + } + + if content != "" { + rawJSON["content"] = content + } else { + delete(rawJSON, "content") + } + + if contentMap != nil { + rawJSON["contentMap"] = contentMap + } else { + delete(rawJSON, "contentMap") + } +} + +// NormalizeOutgoingObjectProp normalizes each Object entry in the rawJSON of the given +// item by calling custom serialization / normalization functions on them in turn. +// +// This function also unnests single-entry arrays, so that: +// +// "object": [ +// { +// ... +// } +// ] +// +// Becomes: +// +// "object": { +// ... +// } +// +// Noop for each Object entry that isn't an Accountable or Statusable. +func NormalizeOutgoingObjectProp(item WithObject, rawJSON map[string]interface{}) error { + objectProp := item.GetActivityStreamsObject() + if objectProp == nil { + // Nothing to do, + // bail early. + return nil + } + + objectPropLen := objectProp.Len() + if objectPropLen == 0 { + // Nothing to do, + // bail early. + return nil + } + + // The thing we already serialized has objects + // on it, so we should see if we need to custom + // serialize any of those objects, and replace + // them on the data map as necessary. + objects := make([]interface{}, 0, objectPropLen) + for iter := objectProp.Begin(); iter != objectProp.End(); iter = iter.Next() { + if iter.IsIRI() { + // Plain IRIs don't need custom serialization. + objects = append(objects, iter.GetIRI().String()) + continue + } + + var ( + objectType = iter.GetType() + objectSer map[string]interface{} + ) + + if objectType == nil { + // This is awkward. + return gtserror.Newf("could not resolve object iter %T to vocab.Type", iter) + } + + var err error + + // In the below accountable and statusable serialization, + // `@context` will be included in the wrapping type already, + // so we shouldn't also include it in the object itself. + switch tn := objectType.GetTypeName(); { + case IsAccountable(tn): + objectSer, err = serializeAccountable(objectType, false) + + case IsStatusable(tn): + // IsStatusable includes Pollable as well. + objectSer, err = serializeStatusable(objectType, false) + + default: + // No custom serializer for this type; serialize as normal. + objectSer, err = objectType.Serialize() + } + + if err != nil { + return err + } + + objects = append(objects, objectSer) + } + + if objectPropLen == 1 { + // Unnest single object. + rawJSON["object"] = objects[0] + } else { + // Array of objects. + rawJSON["object"] = objects + } + + return nil +} diff --git a/internal/ap/normalize_test.go b/internal/ap/normalize_test.go index cd1affe60..33b1f6ea6 100644 --- a/internal/ap/normalize_test.go +++ b/internal/ap/normalize_test.go @@ -46,6 +46,9 @@ func (suite *NormalizeTestSuite) getStatusable() (vocab.ActivityStreamsNote, map "https://example.org/users/someone/followers" ], "content": "UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class=\"hashtag\" data-tag=\"twittermigration\" href=\"https://example.org/tag/twittermigration\" rel=\"tag ugc\">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.", + "contentMap": { + "en": "UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class=\"hashtag\" data-tag=\"twittermigration\" href=\"https://example.org/tag/twittermigration\" rel=\"tag ugc\">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues." + }, "context": "https://example.org/contexts/01GX0MSHPER1E0FT022Q209EJZ", "conversation": "https://example.org/contexts/01GX0MSHPER1E0FT022Q209EJZ", "id": "https://example.org/objects/01GX0MT2PA58JNSMK11MCS65YD", @@ -182,7 +185,15 @@ func (suite *NormalizeTestSuite) getAccountable() (vocab.ActivityStreamsPerson, func (suite *NormalizeTestSuite) TestNormalizeActivityObject() { note, rawNote := suite.getStatusable() - suite.Equal(`update: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" data-tag="twittermigration" href="https://example.org/tag/twittermigration" rel="tag ugc">#TwitterMigration%3C/a%3E.%3Cbr%3E%3Cbr%3EIn%20fact,%20100,000%20new%20accounts%20have%20been%20created%20since%20last%20night.%3Cbr%3E%3Cbr%3ESince%20last%20night&%2339;s%20spike%208,000-12,000%20new%20accounts%20are%20being%20created%20every%20hour.%3Cbr%3E%3Cbr%3EYesterday,%20I%20estimated%20that%20Mastodon%20would%20have%208%20million%20users%20by%20the%20end%20of%20the%20week.%20That%20might%20happen%20a%20lot%20sooner%20if%20this%20trend%20continues.`, ap.ExtractContent(note)) + content := ap.ExtractContent(note) + suite.Equal( + `update: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" data-tag="twittermigration" href="https://example.org/tag/twittermigration" rel="tag ugc">#TwitterMigration%3C/a%3E.%3Cbr%3E%3Cbr%3EIn%20fact,%20100,000%20new%20accounts%20have%20been%20created%20since%20last%20night.%3Cbr%3E%3Cbr%3ESince%20last%20night&%2339;s%20spike%208,000-12,000%20new%20accounts%20are%20being%20created%20every%20hour.%3Cbr%3E%3Cbr%3EYesterday,%20I%20estimated%20that%20Mastodon%20would%20have%208%20million%20users%20by%20the%20end%20of%20the%20week.%20That%20might%20happen%20a%20lot%20sooner%20if%20this%20trend%20continues.`, + content.Content, + ) + + // Malformed contentMap entry + // will not be extractable yet. + suite.Empty(content.ContentMap["en"]) create := testrig.WrapAPNoteInCreate( testrig.URLMustParse("https://example.org/create_something"), @@ -192,7 +203,18 @@ func (suite *NormalizeTestSuite) TestNormalizeActivityObject() { ) ap.NormalizeIncomingActivity(create, map[string]interface{}{"object": rawNote}) - suite.Equal(`UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`, ap.ExtractContent(note)) + content = ap.ExtractContent(note) + + suite.Equal( + `UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`, + content.Content, + ) + + // Content map entry should now be extractable. + suite.Equal( + `UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`, + content.ContentMap["en"], + ) } func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment() { @@ -202,12 +224,14 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment // the attachment(s) should be all jacked up. suite.Equal(`{ "@context": "https://www.w3.org/ns/activitystreams", - "attachment": { - "mediaType": "image/jpeg", - "name": "description: here's \u003c\u003ca\u003e\u003e picture of a #cat,%20it%27s%20cute!%20here%27s%20some%20special%20characters:%20%22%22%20%5C%20weeee%27%27%27%27", - "type": "Document", - "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" - }, + "attachment": [ + { + "mediaType": "image/jpeg", + "name": "description: here's \u003c\u003ca\u003e\u003e picture of a #cat,%20it%27s%20cute!%20here%27s%20some%20special%20characters:%20%22%22%20%5C%20weeee%27%27%27%27", + "type": "Document", + "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" + } + ], "attributedTo": "https://example.org/users/hourlycatbot", "id": "https://example.org/users/hourlycatbot/statuses/01GYW48H311PZ78C5G856MGJJJ", "to": "https://www.w3.org/ns/activitystreams#Public", @@ -222,12 +246,14 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment // attachment should no longer be all jacked up. suite.Equal(`{ "@context": "https://www.w3.org/ns/activitystreams", - "attachment": { - "mediaType": "image/jpeg", - "name": "DESCRIPTION: here's \u003c\u003e picture of a #cat, it's cute! here's some special characters: \"\" \\ weeee''''", - "type": "Document", - "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" - }, + "attachment": [ + { + "mediaType": "image/jpeg", + "name": "DESCRIPTION: here's \u003c\u003e picture of a #cat, it's cute! here's some special characters: \"\" \\ weeee''''", + "type": "Document", + "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" + } + ], "attributedTo": "https://example.org/users/hourlycatbot", "id": "https://example.org/users/hourlycatbot/statuses/01GYW48H311PZ78C5G856MGJJJ", "to": "https://www.w3.org/ns/activitystreams#Public", @@ -243,12 +269,14 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment // the attachment(s) should be all jacked up. suite.Equal(`{ "@context": "https://www.w3.org/ns/activitystreams", - "attachment": { - "mediaType": "image/jpeg", - "name": "description: here's \u003c\u003ca\u003e\u003e picture of a #cat,%20it%27s%20cute!%20here%27s%20some%20special%20characters:%20%22%22%20%5C%20weeee%27%27%27%27", - "type": "Document", - "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" - }, + "attachment": [ + { + "mediaType": "image/jpeg", + "name": "description: here's \u003c\u003ca\u003e\u003e picture of a #cat,%20it%27s%20cute!%20here%27s%20some%20special%20characters:%20%22%22%20%5C%20weeee%27%27%27%27", + "type": "Document", + "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" + } + ], "attributedTo": "https://example.org/users/hourlycatbot", "id": "https://example.org/users/hourlycatbot/statuses/01GYW48H311PZ78C5G856MGJJJ", "to": "https://www.w3.org/ns/activitystreams#Public", @@ -263,12 +291,14 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment // attachment should no longer be all jacked up. suite.Equal(`{ "@context": "https://www.w3.org/ns/activitystreams", - "attachment": { - "mediaType": "image/jpeg", - "name": "DESCRIPTION: here's \u003c\u003e picture of a #cat, it's cute! here's some special characters: \"\" \\ weeee''''", - "type": "Document", - "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" - }, + "attachment": [ + { + "mediaType": "image/jpeg", + "name": "DESCRIPTION: here's \u003c\u003e picture of a #cat, it's cute! here's some special characters: \"\" \\ weeee''''", + "type": "Document", + "url": "https://files.example.org/media_attachments/files/110/258/459/579/509/026/original/b65392ebe0fb04ef.jpeg" + } + ], "attributedTo": "https://example.org/users/hourlycatbot", "id": "https://example.org/users/hourlycatbot/statuses/01GYW48H311PZ78C5G856MGJJJ", "to": "https://www.w3.org/ns/activitystreams#Public", diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go index 368d7f9a2..944e67407 100644 --- a/internal/ap/serialize.go +++ b/internal/ap/serialize.go @@ -18,10 +18,9 @@ package ap import ( - "fmt" - "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) // Serialize is a custom serializer for ActivityStreams types. @@ -35,17 +34,20 @@ import ( // // Currently, the following things will be custom serialized: // -// - OrderedCollection: 'orderedItems' property will always be made into an array. -// - Any Accountable type: 'attachment' property will always be made into an array. -// - Update: any Accountable 'object's set on an update will be custom serialized as above. +// - OrderedCollection: 'orderedItems' property will always be made into an array. +// - Any Accountable type: 'attachment' property will always be made into an array. +// - Any Statusable type: 'attachment' property will always be made into an array; 'content' and 'contentMap' will be normalized. +// - Any Activityable type: any 'object's set on an activity will be custom serialized as above. func Serialize(t vocab.Type) (m map[string]interface{}, e error) { - switch t.GetTypeName() { - case ObjectOrderedCollection: + switch tn := t.GetTypeName(); { + case tn == ObjectOrderedCollection: return serializeOrderedCollection(t) - case ActorApplication, ActorGroup, ActorOrganization, ActorPerson, ActorService: + case IsAccountable(tn): return serializeAccountable(t, true) - case ActivityUpdate: - return serializeWithObject(t) + case IsStatusable(tn): + return serializeStatusable(t, true) + case IsActivityable(tn): + return serializeActivityable(t, true) default: // No custom serializer necessary. return streams.Serialize(t) @@ -61,8 +63,8 @@ func Serialize(t vocab.Type) (m map[string]interface{}, e error) { // See: // - https://github.com/go-fed/activity/issues/139 // - https://github.com/mastodon/mastodon/issues/24225 -func serializeOrderedCollection(orderedCollection vocab.Type) (map[string]interface{}, error) { - data, err := streams.Serialize(orderedCollection) +func serializeOrderedCollection(t vocab.Type) (map[string]interface{}, error) { + data, err := streams.Serialize(t) if err != nil { return nil, err } @@ -99,7 +101,12 @@ func serializeOrderedCollection(orderedCollection vocab.Type) (map[string]interf // If the accountable is being serialized as part of another object (eg., as the // object of an activity), then includeContext should be set to false, as the // @context entry should be included on the top-level/wrapping activity/object. -func serializeAccountable(accountable vocab.Type, includeContext bool) (map[string]interface{}, error) { +func serializeAccountable(t vocab.Type, includeContext bool) (map[string]interface{}, error) { + accountable, ok := t.(Accountable) + if !ok { + return nil, gtserror.Newf("vocab.Type %T not accountable", t) + } + var ( data map[string]interface{} err error @@ -115,91 +122,61 @@ func serializeAccountable(accountable vocab.Type, includeContext bool) (map[stri return nil, err } - attachment, ok := data["attachment"] - if !ok { - // No 'attachment', nothing to change. - return data, nil - } - - if _, ok := attachment.([]interface{}); ok { - // Already slice. - return data, nil - } - - // Coerce single-object to slice. - data["attachment"] = []interface{}{attachment} + NormalizeOutgoingAttachmentProp(accountable, data) return data, nil } -func serializeWithObject(t vocab.Type) (map[string]interface{}, error) { - withObject, ok := t.(WithObject) +func serializeStatusable(t vocab.Type, includeContext bool) (map[string]interface{}, error) { + statusable, ok := t.(Statusable) if !ok { - return nil, fmt.Errorf("serializeWithObject: could not resolve %T to WithObject", t) + return nil, gtserror.Newf("vocab.Type %T not statusable", t) + } + + var ( + data map[string]interface{} + err error + ) + + if includeContext { + data, err = streams.Serialize(statusable) + } else { + data, err = statusable.Serialize() } - data, err := streams.Serialize(t) if err != nil { return nil, err } - object := withObject.GetActivityStreamsObject() - if object == nil { - // Nothing to do, bail early. - return data, nil + NormalizeOutgoingAttachmentProp(statusable, data) + NormalizeOutgoingContentProp(statusable, data) + + return data, nil +} + +func serializeActivityable(t vocab.Type, includeContext bool) (map[string]interface{}, error) { + activityable, ok := t.(Activityable) + if !ok { + return nil, gtserror.Newf("vocab.Type %T not activityable", t) } - objectLen := object.Len() - if objectLen == 0 { - // Nothing to do, bail early. - return data, nil + var ( + data map[string]interface{} + err error + ) + + if includeContext { + data, err = streams.Serialize(activityable) + } else { + data, err = activityable.Serialize() } - // The thing we already serialized has objects - // on it, so we should see if we need to custom - // serialize any of those objects, and replace - // them on the data map as necessary. - objects := make([]interface{}, 0, objectLen) - for iter := object.Begin(); iter != object.End(); iter = iter.Next() { - if iter.IsIRI() { - // Plain IRIs don't need custom serialization. - objects = append(objects, iter.GetIRI().String()) - continue - } - - var ( - objectType = iter.GetType() - objectSer map[string]interface{} - ) - - if objectType == nil { - // This is awkward. - return nil, fmt.Errorf("serializeWithObject: could not resolve object iter %T to vocab.Type", iter) - } - - switch objectType.GetTypeName() { - case ActorApplication, ActorGroup, ActorOrganization, ActorPerson, ActorService: - // @context will be included in wrapping type already, - // we don't need to include it in the object itself. - objectSer, err = serializeAccountable(objectType, false) - default: - // No custom serializer for this type; serialize as normal. - objectSer, err = objectType.Serialize() - } - - if err != nil { - return nil, err - } - - objects = append(objects, objectSer) + if err != nil { + return nil, err } - if objectLen == 1 { - // Unnest single object. - data["object"] = objects[0] - } else { - // Array of objects. - data["object"] = objects + if err := NormalizeOutgoingObjectProp(activityable, data); err != nil { + return nil, err } return data, nil |