mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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
This commit is contained in:
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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) {
|
||||
|
@@ -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 ""
|
||||
}
|
||||
|
||||
// Content should be HTML encoded by default:
|
||||
if content == "" {
|
||||
// Nothing to fix.
|
||||
return ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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
|
||||
)
|
||||
|
||||
// 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 objectLen == 1 {
|
||||
// Unnest single object.
|
||||
data["object"] = objects[0]
|
||||
if includeContext {
|
||||
data, err = streams.Serialize(activityable)
|
||||
} else {
|
||||
// Array of objects.
|
||||
data["object"] = objects
|
||||
data, err = activityable.Serialize()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := NormalizeOutgoingObjectProp(activityable, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
Reference in New Issue
Block a user