summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-05-09 12:16:10 +0200
committerLibravatar GitHub <noreply@github.com>2023-05-09 11:16:10 +0100
commit0e29f1f5bb68a48d9b837d7f4e0a16370734955b (patch)
treef08d203ec8ca8aeea728e5251b1dc3956524b4f4 /internal
parent[chore/performance] Make sender multiplier configurable (#1750) (diff)
downloadgotosocial-0e29f1f5bb68a48d9b837d7f4e0a16370734955b.tar.xz
[feature] Enable federation in/out of profile PropertyValue fields (#1722)
Co-authored-by: kim <grufwub@gmail.com> Co-authored-by: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/ap/extract.go53
-rw-r--r--internal/ap/interfaces.go1
-rw-r--r--internal/ap/normalize.go110
-rw-r--r--internal/ap/normalize_test.go16
-rw-r--r--internal/ap/resolve.go10
-rw-r--r--internal/ap/serialize.go168
-rw-r--r--internal/api/activitypub/users/inboxpost_test.go9
-rw-r--r--internal/api/client/admin/reportsget_test.go52
-rw-r--r--internal/api/model/field.go2
-rw-r--r--internal/db/bundb/migrations/20230430105735_fieldwork.go56
-rw-r--r--internal/federation/dereferencing/account.go99
-rw-r--r--internal/federation/dereferencing/account_test.go10
-rw-r--r--internal/federation/dereferencing/dereferencer.go9
-rw-r--r--internal/federation/dereferencing/status.go4
-rw-r--r--internal/federation/federatingactor.go7
-rw-r--r--internal/federation/federatingdb/followers_test.go4
-rw-r--r--internal/federation/federatingdb/following_test.go4
-rw-r--r--internal/federation/federatingdb/update.go123
-rw-r--r--internal/federation/federatingdb/util.go2
-rw-r--r--internal/federation/federatingprotocol.go2
-rw-r--r--internal/gtsmodel/account.go3
-rw-r--r--internal/messages/messages.go11
-rw-r--r--internal/processing/account/get.go2
-rw-r--r--internal/processing/account/update.go178
-rw-r--r--internal/processing/account/update_test.go80
-rw-r--r--internal/processing/fedi/collections.go11
-rw-r--r--internal/processing/fedi/common.go2
-rw-r--r--internal/processing/fedi/emoji.go4
-rw-r--r--internal/processing/fedi/status.go10
-rw-r--r--internal/processing/fedi/user.go6
-rw-r--r--internal/processing/fromfederator.go36
-rw-r--r--internal/processing/search.go4
-rw-r--r--internal/processing/util.go1
-rw-r--r--internal/text/formatter.go2
-rw-r--r--internal/text/formatter_test.go4
-rw-r--r--internal/text/goldmark_plaintext.go38
-rw-r--r--internal/text/plain.go52
-rw-r--r--internal/text/plain_test.go28
-rw-r--r--internal/transport/controller.go6
-rw-r--r--internal/typeutils/astointernal.go3
-rw-r--r--internal/typeutils/internaltoas.go20
-rw-r--r--internal/typeutils/internaltoas_test.go139
-rw-r--r--internal/typeutils/internaltofrontend.go37
-rw-r--r--internal/typeutils/internaltofrontend_test.go39
-rw-r--r--internal/typeutils/wrap_test.go6
-rw-r--r--internal/validate/account_test.go2
-rw-r--r--internal/validate/formvalidation.go30
-rw-r--r--internal/validate/formvalidation_test.go57
48 files changed, 1151 insertions, 401 deletions
diff --git a/internal/ap/extract.go b/internal/ap/extract.go
index 2742d27ac..ce7c03901 100644
--- a/internal/ap/extract.go
+++ b/internal/ap/extract.go
@@ -266,6 +266,59 @@ func ExtractSummary(i WithSummary) string {
return ""
}
+func ExtractFields(i WithAttachment) []*gtsmodel.Field {
+ attachmentProp := i.GetActivityStreamsAttachment()
+ if attachmentProp == nil {
+ // Nothing to do.
+ return nil
+ }
+
+ l := attachmentProp.Len()
+ if l == 0 {
+ // Nothing to do.
+ return nil
+ }
+
+ fields := make([]*gtsmodel.Field, 0, l)
+ for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
+ if !iter.IsSchemaPropertyValue() {
+ continue
+ }
+
+ propertyValue := iter.GetSchemaPropertyValue()
+ if propertyValue == nil {
+ continue
+ }
+
+ nameProp := propertyValue.GetActivityStreamsName()
+ if nameProp == nil || nameProp.Len() != 1 {
+ continue
+ }
+
+ name := nameProp.At(0).GetXMLSchemaString()
+ if name == "" {
+ continue
+ }
+
+ valueProp := propertyValue.GetSchemaValue()
+ if valueProp == nil || !valueProp.IsXMLSchemaString() {
+ continue
+ }
+
+ value := valueProp.Get()
+ if value == "" {
+ continue
+ }
+
+ fields = append(fields, &gtsmodel.Field{
+ Name: name,
+ Value: value,
+ })
+ }
+
+ return fields
+}
+
// ExtractDiscoverable extracts the Discoverable boolean of an interface.
func ExtractDiscoverable(i WithDiscoverable) (bool, error) {
if i.GetTootDiscoverable() == nil {
diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go
index 674791229..611f4bde0 100644
--- a/internal/ap/interfaces.go
+++ b/internal/ap/interfaces.go
@@ -30,6 +30,7 @@ type Accountable interface {
WithName
WithImage
WithSummary
+ WithAttachment
WithSetSummary
WithDiscoverable
WithURL
diff --git a/internal/ap/normalize.go b/internal/ap/normalize.go
index 52e297080..38861a1b9 100644
--- a/internal/ap/normalize.go
+++ b/internal/ap/normalize.go
@@ -22,14 +22,23 @@ import (
"github.com/superseriousbusiness/activity/streams"
)
-// NormalizeActivityObject normalizes the 'object'.'content' field of the given Activity.
+/*
+ NORMALIZE INCOMING
+ 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
+ instance, or as a result of this instance doing an http call to
+ another instance to dereference something.
+*/
+
+// NormalizeIncomingActivityObject normalizes the 'object'.'content' field of the given Activity.
//
// The rawActivity map should the freshly deserialized json representation of the Activity.
//
-// This function is a noop if the type passed in is anything except a Create with a Statusable as its Object.
-func NormalizeActivityObject(activity pub.Activity, rawJSON map[string]interface{}) {
- if activity.GetTypeName() != ActivityCreate {
- // Only interested in Create right now.
+// This function is a noop if the type passed in is anything except a Create or Update with a Statusable or Accountable as its Object.
+func NormalizeIncomingActivityObject(activity pub.Activity, rawJSON map[string]interface{}) {
+ if typeName := activity.GetTypeName(); typeName != ActivityCreate && typeName != ActivityUpdate {
+ // Only interested in Create or Update right now.
return
}
@@ -51,8 +60,8 @@ func NormalizeActivityObject(activity pub.Activity, rawJSON map[string]interface
}
// We now know length is 1 so get the first
- // item from the iter. We need this to be
- // a Statusable if we're to continue.
+ // item from the iter. We need this to be
+ // a Statusable or Accountable if we're to continue.
i := createObject.At(0)
if i == nil {
// This is awkward.
@@ -65,38 +74,63 @@ func NormalizeActivityObject(activity pub.Activity, rawJSON map[string]interface
return
}
- statusable, ok := t.(Statusable)
- if !ok {
- // Object is not Statusable;
- // we're not interested.
- return
- }
+ switch t.GetTypeName() {
+ case ObjectArticle, ObjectDocument, ObjectImage, ObjectVideo, ObjectNote, ObjectPage, ObjectEvent, ObjectPlace, ObjectProfile:
+ statusable, ok := t.(Statusable)
+ if !ok {
+ // Object is not Statusable;
+ // we're not interested.
+ return
+ }
- rawObject, ok := rawJSON["object"]
- if !ok {
- // No object in raw map.
- return
- }
+ rawObject, ok := rawJSON["object"]
+ if !ok {
+ // No object in raw map.
+ return
+ }
- rawStatusableJSON, ok := rawObject.(map[string]interface{})
- if !ok {
- // Object wasn't a json object.
- return
- }
+ rawStatusableJSON, ok := rawObject.(map[string]interface{})
+ if !ok {
+ // Object wasn't a json object.
+ return
+ }
- // Normalize everything we can on the statusable.
- NormalizeContent(statusable, rawStatusableJSON)
- NormalizeAttachments(statusable, rawStatusableJSON)
- NormalizeSummary(statusable, rawStatusableJSON)
- NormalizeName(statusable, rawStatusableJSON)
+ // Normalize everything we can on the statusable.
+ NormalizeIncomingContent(statusable, rawStatusableJSON)
+ NormalizeIncomingAttachments(statusable, rawStatusableJSON)
+ NormalizeIncomingSummary(statusable, rawStatusableJSON)
+ NormalizeIncomingName(statusable, rawStatusableJSON)
+ case ActorApplication, ActorGroup, ActorOrganization, ActorPerson, ActorService:
+ accountable, ok := t.(Accountable)
+ if !ok {
+ // Object is not Accountable;
+ // we're not interested.
+ return
+ }
+
+ rawObject, ok := rawJSON["object"]
+ if !ok {
+ // No object in raw map.
+ return
+ }
+
+ rawAccountableJSON, ok := rawObject.(map[string]interface{})
+ if !ok {
+ // Object wasn't a json object.
+ return
+ }
+
+ // Normalize everything we can on the accountable.
+ NormalizeIncomingSummary(accountable, rawAccountableJSON)
+ }
}
-// NormalizeContent replaces the Content of the given item
+// NormalizeIncomingContent replaces the Content of the given item
// with the raw 'content' value from the raw json object map.
//
// noop if there was no content in the json object map or the
// content was not a plain string.
-func NormalizeContent(item WithSetContent, rawJSON map[string]interface{}) {
+func NormalizeIncomingContent(item WithSetContent, rawJSON map[string]interface{}) {
rawContent, ok := rawJSON["content"]
if !ok {
// No content in rawJSON.
@@ -118,13 +152,13 @@ func NormalizeContent(item WithSetContent, rawJSON map[string]interface{}) {
item.SetActivityStreamsContent(contentProp)
}
-// NormalizeAttachments normalizes all attachments (if any) of the given
-// itm, replacing the 'name' (aka content warning) field of each attachment
+// NormalizeIncomingAttachments normalizes all attachments (if any) of the given
+// item, replacing the 'name' (aka content warning) field of each attachment
// with the raw 'name' value from the raw json object map.
//
// noop if there are no attachments; noop if attachment is not a format
// we can understand.
-func NormalizeAttachments(item WithAttachment, rawJSON map[string]interface{}) {
+func NormalizeIncomingAttachments(item WithAttachment, rawJSON map[string]interface{}) {
rawAttachments, ok := rawJSON["attachment"]
if !ok {
// No attachments in rawJSON.
@@ -173,16 +207,16 @@ func NormalizeAttachments(item WithAttachment, rawJSON map[string]interface{}) {
continue
}
- NormalizeName(attachmentable, rawAttachment)
+ NormalizeIncomingName(attachmentable, rawAttachment)
}
}
-// NormalizeSummary replaces the Summary of the given item
+// NormalizeIncomingSummary replaces the Summary of the given item
// with the raw 'summary' value from the raw json object map.
//
// noop if there was no summary in the json object map or the
// summary was not a plain string.
-func NormalizeSummary(item WithSetSummary, rawJSON map[string]interface{}) {
+func NormalizeIncomingSummary(item WithSetSummary, rawJSON map[string]interface{}) {
rawSummary, ok := rawJSON["summary"]
if !ok {
// No summary in rawJSON.
@@ -202,12 +236,12 @@ func NormalizeSummary(item WithSetSummary, rawJSON map[string]interface{}) {
item.SetActivityStreamsSummary(summaryProp)
}
-// NormalizeName replaces the Name of the given item
+// NormalizeIncomingName replaces the Name of the given item
// with the raw 'name' value from the raw json object map.
//
// noop if there was no name in the json object map or the
// name was not a plain string.
-func NormalizeName(item WithSetName, rawJSON map[string]interface{}) {
+func NormalizeIncomingName(item WithSetName, rawJSON map[string]interface{}) {
rawName, ok := rawJSON["name"]
if !ok {
// No name in rawJSON.
diff --git a/internal/ap/normalize_test.go b/internal/ap/normalize_test.go
index c265b02f5..2c9a1907a 100644
--- a/internal/ap/normalize_test.go
+++ b/internal/ap/normalize_test.go
@@ -49,7 +49,7 @@ func (suite *NormalizeTestSuite) jsonToType(rawJson string) (vocab.Type, map[str
}
func (suite *NormalizeTestSuite) typeToJson(t vocab.Type) string {
- m, err := streams.Serialize(t)
+ m, err := ap.Serialize(t)
if err != nil {
suite.FailNow(err.Error())
}
@@ -223,7 +223,7 @@ func (suite *NormalizeTestSuite) TestNormalizeActivityObject() {
note,
)
- ap.NormalizeActivityObject(create, map[string]interface{}{"object": rawNote})
+ ap.NormalizeIncomingActivityObject(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" 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&#39;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))
}
@@ -248,7 +248,7 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment
}`, suite.typeToJson(note))
// Normalize it!
- ap.NormalizeAttachments(note, rawNote)
+ ap.NormalizeIncomingAttachments(note, rawNote)
// After normalization, the 'name' field of the
// attachment should no longer be all jacked up.
@@ -289,7 +289,7 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsOneAttachment
}`, suite.typeToJson(note))
// Normalize it!
- ap.NormalizeAttachments(note, rawNote)
+ ap.NormalizeIncomingAttachments(note, rawNote)
// After normalization, the 'name' field of the
// attachment should no longer be all jacked up.
@@ -349,7 +349,7 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableAttachmentsMultipleAttac
}`, suite.typeToJson(note))
// Normalize it!
- ap.NormalizeAttachments(note, rawNote)
+ ap.NormalizeIncomingAttachments(note, rawNote)
// After normalization, the 'name' field of the
// attachment should no longer be all jacked up.
@@ -392,7 +392,7 @@ func (suite *NormalizeTestSuite) TestNormalizeAccountableSummary() {
accountable, rawAccount := suite.getAccountable()
suite.Equal(`about: I'm a #Barbie%20%23girl%20in%20a%20%23Barbie%20%23world%0ALife%20in%20plastic,%20it%27s%20fantastic%0AYou%20can%20brush%20my%20hair,%20undress%20me%20everywhere%0AImagination,%20life%20is%20your%20creation%0AI%27m%20a%20blonde%20bimbo%20girl%0AIn%20a%20fantasy%20world%0ADress%20me%20up,%20make%20it%20tight%0AI%27m%20your%20dolly%0AYou%27re%20my%20doll,%20rock%20and%20roll%0AFeel%20the%20glamour%20in%20pink%0AKiss%20me%20here,%20touch%20me%20there%0AHanky%20panky`, ap.ExtractSummary(accountable))
- ap.NormalizeSummary(accountable, rawAccount)
+ ap.NormalizeIncomingSummary(accountable, rawAccount)
suite.Equal(`about: I'm a #Barbie #girl in a #Barbie #world
Life in plastic, it's fantastic
You can brush my hair, undress me everywhere
@@ -411,7 +411,7 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableSummary() {
statusable, rawAccount := suite.getStatusableWithWeirdSummaryAndName()
suite.Equal(`warning: #WEIRD%20%23SUMMARY%20;;;;a;;a;asv%20%20%20%20khop8273987(*%5E&%5E)`, ap.ExtractSummary(statusable))
- ap.NormalizeSummary(statusable, rawAccount)
+ ap.NormalizeIncomingSummary(statusable, rawAccount)
suite.Equal(`warning: #WEIRD #SUMMARY ;;;;a;;a;asv khop8273987(*^&^)`, ap.ExtractSummary(statusable))
}
@@ -419,7 +419,7 @@ func (suite *NormalizeTestSuite) TestNormalizeStatusableName() {
statusable, rawAccount := suite.getStatusableWithWeirdSummaryAndName()
suite.Equal(`warning: #WEIRD%20%23nameEE%20;;;;a;;a;asv%20%20%20%20khop8273987(*%5E&%5E)`, ap.ExtractName(statusable))
- ap.NormalizeName(statusable, rawAccount)
+ ap.NormalizeIncomingName(statusable, rawAccount)
suite.Equal(`WARNING: #WEIRD #nameEE ;;;;a;;a;asv khop8273987(*^&^)`, ap.ExtractName(statusable))
}
diff --git a/internal/ap/resolve.go b/internal/ap/resolve.go
index 8d116751c..ef4d0b50f 100644
--- a/internal/ap/resolve.go
+++ b/internal/ap/resolve.go
@@ -72,10 +72,10 @@ func ResolveStatusable(ctx context.Context, b []byte) (Statusable, error) {
return nil, newErrWrongType(err)
}
- NormalizeContent(statusable, rawStatusable)
- NormalizeAttachments(statusable, rawStatusable)
- NormalizeSummary(statusable, rawStatusable)
- NormalizeName(statusable, rawStatusable)
+ NormalizeIncomingContent(statusable, rawStatusable)
+ NormalizeIncomingAttachments(statusable, rawStatusable)
+ NormalizeIncomingSummary(statusable, rawStatusable)
+ NormalizeIncomingName(statusable, rawStatusable)
return statusable, nil
}
@@ -118,7 +118,7 @@ func ResolveAccountable(ctx context.Context, b []byte) (Accountable, error) {
return nil, newErrWrongType(err)
}
- NormalizeSummary(accountable, rawAccountable)
+ NormalizeIncomingSummary(accountable, rawAccountable)
return accountable, nil
}
diff --git a/internal/ap/serialize.go b/internal/ap/serialize.go
index 471a7f41e..368d7f9a2 100644
--- a/internal/ap/serialize.go
+++ b/internal/ap/serialize.go
@@ -18,13 +18,41 @@
package ap
import (
- "errors"
+ "fmt"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
)
-// SerializeOrderedCollection is a custom serializer for an ActivityStreamsOrderedCollection.
+// Serialize is a custom serializer for ActivityStreams types.
+//
+// In most cases, it will simply call the go-fed streams.Serialize function under the hood.
+// However, if custom serialization is required on a specific type (eg for inter-implementation
+// compatibility), it can be inserted into the switch as necessary.
+//
+// Callers should always call this function instead of streams.Serialize, unless there's a
+// very good reason to do otherwise.
+//
+// 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.
+func Serialize(t vocab.Type) (m map[string]interface{}, e error) {
+ switch t.GetTypeName() {
+ case ObjectOrderedCollection:
+ return serializeOrderedCollection(t)
+ case ActorApplication, ActorGroup, ActorOrganization, ActorPerson, ActorService:
+ return serializeAccountable(t, true)
+ case ActivityUpdate:
+ return serializeWithObject(t)
+ default:
+ // No custom serializer necessary.
+ return streams.Serialize(t)
+ }
+}
+
+// serializeOrderedCollection is a custom serializer for an ActivityStreamsOrderedCollection.
// Unlike the standard streams.Serialize function, this serializer normalizes the orderedItems
// value to always be an array/slice, regardless of how many items are contained therein.
//
@@ -33,32 +61,146 @@ import (
// See:
// - https://github.com/go-fed/activity/issues/139
// - https://github.com/mastodon/mastodon/issues/24225
-func SerializeOrderedCollection(orderedCollection vocab.ActivityStreamsOrderedCollection) (map[string]interface{}, error) {
+func serializeOrderedCollection(orderedCollection vocab.Type) (map[string]interface{}, error) {
data, err := streams.Serialize(orderedCollection)
if err != nil {
return nil, err
}
- return data, normalizeOrderedCollectionData(data)
+ orderedItems, ok := data["orderedItems"]
+ if !ok {
+ // No 'orderedItems', nothing to change.
+ return data, nil
+ }
+
+ if _, ok := orderedItems.([]interface{}); ok {
+ // Already slice.
+ return data, nil
+ }
+
+ // Coerce single-object to slice.
+ data["orderedItems"] = []interface{}{orderedItems}
+
+ return data, nil
}
-func normalizeOrderedCollectionData(rawOrderedCollection map[string]interface{}) error {
- orderedItems, ok := rawOrderedCollection["orderedItems"]
+// SerializeAccountable is a custom serializer for any Accountable type.
+// This serializer rewrites the 'attachment' value of the Accountable, if
+// present, to always be an array/slice.
+//
+// While this is not strictly necessary in json-ld terms, most other fedi
+// implementations look for attachment to be an array of PropertyValue (field)
+// entries, and will not parse single-entry, non-array attachments on accounts
+// properly.
+//
+// If the accountable is being serialized as a top-level object (eg., for serving
+// in response to an account dereference request), then includeContext should be
+// set to true, so as to include the json-ld '@context' entries in the data.
+// 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) {
+ var (
+ data map[string]interface{}
+ err error
+ )
+
+ if includeContext {
+ data, err = streams.Serialize(accountable)
+ } else {
+ data, err = accountable.Serialize()
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ attachment, ok := data["attachment"]
if !ok {
- return errors.New("no orderedItems set on OrderedCollection")
+ // No 'attachment', nothing to change.
+ return data, nil
}
- if _, ok := orderedItems.([]interface{}); ok {
+ if _, ok := attachment.([]interface{}); ok {
// Already slice.
- return nil
+ return data, nil
}
- orderedItemsString, ok := orderedItems.(string)
+ // Coerce single-object to slice.
+ data["attachment"] = []interface{}{attachment}
+
+ return data, nil
+}
+
+func serializeWithObject(t vocab.Type) (map[string]interface{}, error) {
+ withObject, ok := t.(WithObject)
if !ok {
- return errors.New("orderedItems was neither slice nor string")
+ return nil, fmt.Errorf("serializeWithObject: could not resolve %T to WithObject", t)
+ }
+
+ 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
+ }
+
+ objectLen := object.Len()
+ if objectLen == 0 {
+ // Nothing to do, bail early.
+ return data, nil
}
- rawOrderedCollection["orderedItems"] = []string{orderedItemsString}
+ // 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]
+ } else {
+ // Array of objects.
+ data["object"] = objects
+ }
- return nil
+ return data, nil
}
diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go
index 129563532..26c4029a2 100644
--- a/internal/api/activitypub/users/inboxpost_test.go
+++ b/internal/api/activitypub/users/inboxpost_test.go
@@ -32,6 +32,7 @@ import (
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@@ -76,7 +77,7 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
- bodyI, err := streams.Serialize(block)
+ bodyI, err := ap.Serialize(block)
suite.NoError(err)
bodyJson, err := json.Marshal(bodyI)
@@ -177,7 +178,7 @@ func (suite *InboxPostTestSuite) TestPostUnblock() {
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(undo, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
- bodyI, err := streams.Serialize(undo)
+ bodyI, err := ap.Serialize(undo)
suite.NoError(err)
bodyJson, err := json.Marshal(bodyI)
@@ -275,7 +276,7 @@ func (suite *InboxPostTestSuite) TestPostUpdate() {
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(update, updatedAccount.PublicKeyURI, updatedAccount.PrivateKey, targetURI)
- bodyI, err := streams.Serialize(update)
+ bodyI, err := ap.Serialize(update)
suite.NoError(err)
bodyJson, err := json.Marshal(bodyI)
@@ -412,7 +413,7 @@ func (suite *InboxPostTestSuite) TestPostDelete() {
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(delete, deletedAccount.PublicKeyURI, deletedAccount.PrivateKey, targetURI)
- bodyI, err := streams.Serialize(delete)
+ bodyI, err := ap.Serialize(delete)
suite.NoError(err)
bodyJson, err := json.Marshal(bodyI)
diff --git a/internal/api/client/admin/reportsget_test.go b/internal/api/client/admin/reportsget_test.go
index 67e6103ae..fae21dc07 100644
--- a/internal/api/client/admin/reportsget_test.go
+++ b/internal/api/client/admin/reportsget_test.go
@@ -224,7 +224,18 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
@@ -374,7 +385,18 @@ func (suite *ReportsGetTestSuite) TestReportsGetAll() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
@@ -575,7 +597,18 @@ func (suite *ReportsGetTestSuite) TestReportsGetCreatedByAccount() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
@@ -776,7 +809,18 @@ func (suite *ReportsGetTestSuite) TestReportsGetTargetAccount() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
diff --git a/internal/api/model/field.go b/internal/api/model/field.go
index 399fef20a..c3dc8d67a 100644
--- a/internal/api/model/field.go
+++ b/internal/api/model/field.go
@@ -29,5 +29,5 @@ type Field struct {
Value string `json:"value"`
// If this field has been verified, when did this occur? (ISO 8601 Datetime).
// example: 2021-07-30T09:20:25+00:00
- VerifiedAt string `json:"verified_at,omitempty"`
+ VerifiedAt *string `json:"verified_at"`
}
diff --git a/internal/db/bundb/migrations/20230430105735_fieldwork.go b/internal/db/bundb/migrations/20230430105735_fieldwork.go
new file mode 100644
index 000000000..a560ec62b
--- /dev/null
+++ b/internal/db/bundb/migrations/20230430105735_fieldwork.go
@@ -0,0 +1,56 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package migrations
+
+import (
+ "context"
+ "strings"
+
+ "github.com/uptrace/bun"
+ "github.com/uptrace/bun/dialect"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ var err error
+ switch db.Dialect().Name() {
+ case dialect.SQLite:
+ _, err = db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? VARCHAR", bun.Ident("accounts"), bun.Ident("fields_raw"))
+ case dialect.PG:
+ _, err = db.ExecContext(ctx, "ALTER TABLE ? ADD COLUMN ? JSONB", bun.Ident("accounts"), bun.Ident("fields_raw"))
+ default:
+ panic("db conn was neither pg not sqlite")
+ }
+
+ if err != nil && !(strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "duplicate column name") || strings.Contains(err.Error(), "SQLSTATE 42701")) {
+ return err
+ }
+
+ return nil
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go
index 4795db733..b33f1d6fa 100644
--- a/internal/federation/dereferencing/account.go
+++ b/internal/federation/dereferencing/account.go
@@ -38,7 +38,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/transport"
)
-func (d *deref) GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL, block bool) (*gtsmodel.Account, error) {
+func (d *deref) GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, error) {
var (
account *gtsmodel.Account
uriStr = uri.String()
@@ -70,11 +70,11 @@ func (d *deref) GetAccountByURI(ctx context.Context, requestUser string, uri *ur
ID: id.NewULID(),
Domain: uri.Host,
URI: uriStr,
- }, false, true)
+ }, d.defaultFetchLatest, false)
}
// Try to update existing account model
- enriched, err := d.enrichAccount(ctx, requestUser, uri, account, false, block)
+ enriched, err := d.enrichAccount(ctx, requestUser, uri, account, d.defaultFetchLatest, false)
if err != nil {
log.Errorf(ctx, "error enriching remote account: %v", err)
return account, nil // fall back to returning existing
@@ -83,7 +83,7 @@ func (d *deref) GetAccountByURI(ctx context.Context, requestUser string, uri *ur
return enriched, nil
}
-func (d *deref) GetAccountByUsernameDomain(ctx context.Context, requestUser string, username string, domain string, block bool) (*gtsmodel.Account, error) {
+func (d *deref) GetAccountByUsernameDomain(ctx context.Context, requestUser string, username string, domain string) (*gtsmodel.Account, error) {
if domain == config.GetHost() || domain == config.GetAccountDomain() {
// We do local lookups using an empty domain,
// else it will fail the db search below.
@@ -99,19 +99,24 @@ func (d *deref) GetAccountByUsernameDomain(ctx context.Context, requestUser stri
if account == nil {
// Check for failed local lookup.
if domain == "" {
- return nil, NewErrNotRetrievable(err) // will be db.ErrNoEntries
+ return nil, NewErrNotRetrievable(err) // wrapped err will be db.ErrNoEntries
}
// Create and pass-through a new bare-bones model for dereferencing.
- return d.enrichAccount(ctx, requestUser, nil, &gtsmodel.Account{
+ account = &gtsmodel.Account{
ID: id.NewULID(),
Username: username,
Domain: domain,
- }, false, true)
+ }
+
+ // There's no known account to fall back on,
+ // so return error if we can't enrich account.
+ return d.enrichAccount(ctx, requestUser, nil, account, d.defaultFetchLatest, false)
}
- // Try to update existing account model
- enriched, err := d.enrichAccount(ctx, requestUser, nil, account, false, block)
+ // We knew about this account already;
+ // try to update existing account model.
+ enriched, err := d.enrichAccount(ctx, requestUser, nil, account, d.defaultFetchLatest, false)
if err != nil {
log.Errorf(ctx, "error enriching account from remote: %v", err)
return account, nil // fall back to returning unchanged existing account model
@@ -120,12 +125,62 @@ func (d *deref) GetAccountByUsernameDomain(ctx context.Context, requestUser stri
return enriched, nil
}
-func (d *deref) UpdateAccount(ctx context.Context, requestUser string, account *gtsmodel.Account, force bool) (*gtsmodel.Account, error) {
- return d.enrichAccount(ctx, requestUser, nil, account, force, false)
+func (d *deref) RefreshAccount(ctx context.Context, requestUser string, accountable ap.Accountable, account *gtsmodel.Account) (*gtsmodel.Account, error) {
+ // To avoid unnecessarily refetching multiple times from remote,
+ // we can just pass in the Accountable object that we received,
+ // if it was defined. If not, fall back to default fetch func.
+ var f fetchLatest
+ if accountable != nil {
+ f = func(
+ _ context.Context,
+ _ transport.Transport,
+ _ *url.URL,
+ _ string,
+ ) (ap.Accountable, *gtsmodel.Account, error) {
+ return accountable, account, nil
+ }
+ } else {
+ f = d.defaultFetchLatest
+ }
+
+ // Set 'force' to 'true' to always fetch latest media etc.
+ return d.enrichAccount(ctx, requestUser, nil, account, f, true)
+}
+
+// fetchLatest defines a function for using a transport and uri to fetch the fetchLatest
+// version of an account (and its AP representation) from a remote instance.
+type fetchLatest func(ctx context.Context, transport transport.Transport, uri *url.URL, accountDomain string) (ap.Accountable, *gtsmodel.Account, error)
+
+// defaultFetchLatest deduplicates latest fetching code that is used in several
+// different functions. It simply calls the remote uri using the given transport,
+// parses a returned AP representation into an account, and then returns both.
+func (d *deref) defaultFetchLatest(ctx context.Context, transport transport.Transport, uri *url.URL, accountDomain string) (ap.Accountable, *gtsmodel.Account, error) {
+ // Dereference this account to get the latest available.
+ apubAcc, err := d.dereferenceAccountable(ctx, transport, uri)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error dereferencing account %s: %w", uri, err)
+ }
+
+ // Convert the dereferenced AP account object to our GTS model.
+ latestAcc, err := d.typeConverter.ASRepresentationToAccount(
+ ctx, apubAcc, accountDomain,
+ )
+ if err != nil {
+ return nil, nil, fmt.Errorf("error converting accountable to gts model for account %s: %w", uri, err)
+ }
+
+ return apubAcc, latestAcc, nil
}
// enrichAccount will ensure the given account is the most up-to-date model of the account, re-webfingering and re-dereferencing if necessary.
-func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.URL, account *gtsmodel.Account, force, block bool) (*gtsmodel.Account, error) {
+func (d *deref) enrichAccount(
+ ctx context.Context,
+ requestUser string,
+ uri *url.URL,
+ account *gtsmodel.Account,
+ f fetchLatest,
+ force bool,
+) (*gtsmodel.Account, error) {
if account.IsLocal() {
// Can't update local accounts.
return account, nil
@@ -205,18 +260,10 @@ func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.
d.startHandshake(requestUser, uri)
defer d.stopHandshake(requestUser, uri)
- // Dereference this account to get the latest available.
- apubAcc, err := d.dereferenceAccountable(ctx, transport, uri)
- if err != nil {
- return nil, fmt.Errorf("enrichAccount: error dereferencing account %s: %w", uri, err)
- }
-
- // Convert the dereferenced AP account object to our GTS model.
- latestAcc, err := d.typeConverter.ASRepresentationToAccount(
- ctx, apubAcc, account.Domain,
- )
+ // Fetch latest version of the account, dereferencing if necessary.
+ apubAcc, latestAcc, err := f(ctx, transport, uri, account.Domain)
if err != nil {
- return nil, fmt.Errorf("enrichAccount: error converting accountable to gts model for account %s: %w", uri, err)
+ return nil, fmt.Errorf("enrichAccount: error calling fetchLatest function: %w", err)
}
if account.Username == "" {
@@ -256,11 +303,11 @@ func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.
latestAcc.ID = account.ID
latestAcc.FetchedAt = time.Now()
- // Use the existing account media attachments by default.
+ // Reuse the existing account media attachments by default.
latestAcc.AvatarMediaAttachmentID = account.AvatarMediaAttachmentID
latestAcc.HeaderMediaAttachmentID = account.HeaderMediaAttachmentID
- if latestAcc.AvatarRemoteURL != account.AvatarRemoteURL {
+ if force || (latestAcc.AvatarRemoteURL != account.AvatarRemoteURL) {
// Reset the avatar media ID (handles removed).
latestAcc.AvatarMediaAttachmentID = ""
@@ -281,7 +328,7 @@ func (d *deref) enrichAccount(ctx context.Context, requestUser string, uri *url.
}
}
- if latestAcc.HeaderRemoteURL != account.HeaderRemoteURL {
+ if force || (latestAcc.HeaderRemoteURL != account.HeaderRemoteURL) {
// Reset the header media ID (handles removed).
latestAcc.HeaderMediaAttachmentID = ""
diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go
index ce399f308..c4b946f5b 100644
--- a/internal/federation/dereferencing/account_test.go
+++ b/internal/federation/dereferencing/account_test.go
@@ -41,7 +41,6 @@ func (suite *AccountTestSuite) TestDereferenceGroup() {
context.Background(),
fetchingAccount.Username,
groupURL,
- false,
)
suite.NoError(err)
suite.NotNil(group)
@@ -66,7 +65,6 @@ func (suite *AccountTestSuite) TestDereferenceService() {
context.Background(),
fetchingAccount.Username,
serviceURL,
- false,
)
suite.NoError(err)
suite.NotNil(service)
@@ -99,7 +97,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURL() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
- false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -119,7 +116,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsRemoteURLNoSharedInb
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
- false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -134,7 +130,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsername() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
- false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -149,7 +144,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsernameDomain() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse(targetAccount.URI),
- false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -165,7 +159,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountAsUsernameDomainAndURL
fetchingAccount.Username,
targetAccount.Username,
config.GetHost(),
- false,
)
suite.NoError(err)
suite.NotNil(fetchedAccount)
@@ -180,7 +173,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUsername()
fetchingAccount.Username,
"thisaccountdoesnotexist",
config.GetHost(),
- false,
)
var errNotRetrievable *dereferencing.ErrNotRetrievable
suite.ErrorAs(err, &errNotRetrievable)
@@ -196,7 +188,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUsernameDom
fetchingAccount.Username,
"thisaccountdoesnotexist",
"localhost:8080",
- false,
)
var errNotRetrievable *dereferencing.ErrNotRetrievable
suite.ErrorAs(err, &errNotRetrievable)
@@ -211,7 +202,6 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() {
context.Background(),
fetchingAccount.Username,
testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"),
- false,
)
var errNotRetrievable *dereferencing.ErrNotRetrievable
suite.ErrorAs(err, &errNotRetrievable)
diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go
index 7c00ceec5..181196caa 100644
--- a/internal/federation/dereferencing/dereferencer.go
+++ b/internal/federation/dereferencing/dereferencer.go
@@ -35,14 +35,15 @@ import (
type Dereferencer interface {
// GetAccountByURI will attempt to fetch an account by its URI, first checking the database and in the case of a remote account will either check the
// last_fetched (and updating if beyond fetch interval) or dereferencing for the first-time if this remote account has never been encountered before.
- GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL, block bool) (*gtsmodel.Account, error)
+ GetAccountByURI(ctx context.Context, requestUser string, uri *url.URL) (*gtsmodel.Account, error)
// GetAccountByUsernameDomain will attempt to fetch an account by username@domain, first checking the database and in the case of a remote account will either
// check the last_fetched (and updating if beyond fetch interval) or dereferencing for the first-time if this remote account has never been encountered before.
- GetAccountByUsernameDomain(ctx context.Context, requestUser string, username string, domain string, block bool) (*gtsmodel.Account, error)
+ GetAccountByUsernameDomain(ctx context.Context, requestUser string, username string, domain string) (*gtsmodel.Account, error)
- // UpdateAccount updates the given account if last_fetched is beyond fetch interval (or if force is set). An updated account model is returned, any media fetching is done async.
- UpdateAccount(ctx context.Context, requestUser string, account *gtsmodel.Account, force bool) (*gtsmodel.Account, error)
+ // RefreshAccount forces a refresh of the given account by fetching the current/latest state of the account from the remote instance.
+ // An updated account model is returned, but not yet inserted/updated in the database; this is the caller's responsibility.
+ RefreshAccount(ctx context.Context, requestUser string, accountable ap.Accountable, account *gtsmodel.Account) (*gtsmodel.Account, error)
GetStatus(ctx context.Context, username string, remoteStatusID *url.URL, refetch, includeParent bool) (*gtsmodel.Status, ap.Statusable, error)
diff --git a/internal/federation/dereferencing/status.go b/internal/federation/dereferencing/status.go
index fe07be23a..8e130393a 100644
--- a/internal/federation/dereferencing/status.go
+++ b/internal/federation/dereferencing/status.go
@@ -125,7 +125,7 @@ func (d *deref) GetStatus(ctx context.Context, username string, statusURI *url.U
}
// we need to get the author of the status else we can't serialize it properly
- if _, err = d.GetAccountByURI(ctx, username, accountURI, true); err != nil {
+ if _, err = d.GetAccountByURI(ctx, username, accountURI); err != nil {
return nil, nil, newErrOther(fmt.Errorf("GetRemoteStatus: couldn't get status author: %s", err))
}
@@ -278,7 +278,7 @@ func (d *deref) populateStatusMentions(ctx context.Context, status *gtsmodel.Sta
if targetAccount == nil {
// we didn't find the account in our database already
// check if we can get the account remotely (dereference it)
- if a, err := d.GetAccountByURI(ctx, requestingUsername, targetAccountURI, false); err != nil {
+ if a, err := d.GetAccountByURI(ctx, requestingUsername, targetAccountURI); err != nil {
errs = append(errs, err.Error())
} else {
log.Debugf(ctx, "got target account %s with id %s through GetRemoteAccount", targetAccountURI, a.ID)
diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go
index 18cdf2106..33ae38220 100644
--- a/internal/federation/federatingactor.go
+++ b/internal/federation/federatingactor.go
@@ -59,11 +59,11 @@ type federatingActor struct {
// implements the pub.FederatingActor interface.
func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
sideEffectActor := pub.NewSideEffectActor(c, s2s, nil, db, clock)
- customActor := pub.NewCustomActor(sideEffectActor, false, true, clock)
+ sideEffectActor.Serialize = ap.Serialize // hook in our own custom Serialize function
return &federatingActor{
sideEffectActor: sideEffectActor,
- wrapped: customActor,
+ wrapped: pub.NewCustomActor(sideEffectActor, false, true, clock),
}
}
@@ -165,7 +165,8 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// If activity Object is a Statusable, we'll want to replace the
// parsed `content` value with the value from the raw JSON instead.
// See https://github.com/superseriousbusiness/gotosocial/issues/1661
- ap.NormalizeActivityObject(activity, rawActivity)
+ // Likewise, if it's an Accountable, we'll normalize some fields on it.
+ ap.NormalizeIncomingActivityObject(activity, rawActivity)
// Allow server implementations to set context data with a hook.
ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
diff --git a/internal/federation/federatingdb/followers_test.go b/internal/federation/federatingdb/followers_test.go
index fb660d147..f6297ec8c 100644
--- a/internal/federation/federatingdb/followers_test.go
+++ b/internal/federation/federatingdb/followers_test.go
@@ -23,7 +23,7 @@ import (
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -37,7 +37,7 @@ func (suite *FollowersTestSuite) TestGetFollowers() {
f, err := suite.federatingDB.Followers(context.Background(), testrig.URLMustParse(testAccount.URI))
suite.NoError(err)
- fi, err := streams.Serialize(f)
+ fi, err := ap.Serialize(f)
suite.NoError(err)
fJson, err := json.MarshalIndent(fi, "", " ")
diff --git a/internal/federation/federatingdb/following_test.go b/internal/federation/federatingdb/following_test.go
index ae616b39f..83d1a72b5 100644
--- a/internal/federation/federatingdb/following_test.go
+++ b/internal/federation/federatingdb/following_test.go
@@ -23,7 +23,7 @@ import (
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -37,7 +37,7 @@ func (suite *FollowingTestSuite) TestGetFollowing() {
f, err := suite.federatingDB.Following(context.Background(), testrig.URLMustParse(testAccount.URI))
suite.NoError(err)
- fi, err := streams.Serialize(f)
+ fi, err := ap.Serialize(f)
suite.NoError(err)
fJson, err := json.MarshalIndent(fi, "", " ")
diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go
index 5121418e5..5c8785c08 100644
--- a/internal/federation/federatingdb/update.go
+++ b/internal/federation/federatingdb/update.go
@@ -54,96 +54,69 @@ func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
receivingAccount, _ := extractFromCtx(ctx)
if receivingAccount == nil {
- // If the receiving account wasn't set on the context, that means this request didn't pass
- // through the API, but came from inside GtS as the result of another activity on this instance. That being so,
- // we can safely just ignore this activity, since we know we've already processed it elsewhere.
+ // If the receiving account wasn't set on the context, that means
+ // this request didn't pass through the API, but came from inside
+ // GtS as the result of another activity on this instance. As such,
+ // we must have already processed it in order to reach this stage.
return nil
}
requestingAcctI := ctx.Value(ap.ContextRequestingAccount)
if requestingAcctI == nil {
- l.Error("UPDATE: requesting account wasn't set on context")
+ return errors.New("Update: requesting account wasn't set on context")
}
+
requestingAcct, ok := requestingAcctI.(*gtsmodel.Account)
if !ok {
- l.Error("UPDATE: requesting account was set on context but couldn't be parsed")
+ return errors.New("Update: requesting account was set on context but couldn't be parsed")
}
- typeName := asType.GetTypeName()
- if typeName == ap.ActorApplication ||
- typeName == ap.ActorGroup ||
- typeName == ap.ActorOrganization ||
- typeName == ap.ActorPerson ||
- typeName == ap.ActorService {
- // it's an UPDATE to some kind of account
- var accountable ap.Accountable
- switch typeName {
- case ap.ActorApplication:
- l.Debug("got update for APPLICATION")
- i, ok := asType.(vocab.ActivityStreamsApplication)
- if !ok {
- return errors.New("UPDATE: could not convert type to application")
- }
- accountable = i
- case ap.ActorGroup:
- l.Debug("got update for GROUP")
- i, ok := asType.(vocab.ActivityStreamsGroup)
- if !ok {
- return errors.New("UPDATE: could not convert type to group")
- }
- accountable = i
- case ap.ActorOrganization:
- l.Debug("got update for ORGANIZATION")
- i, ok := asType.(vocab.ActivityStreamsOrganization)
- if !ok {
- return errors.New("UPDATE: could not convert type to organization")
- }
- accountable = i
- case ap.ActorPerson:
- l.Debug("got update for PERSON")
- i, ok := asType.(vocab.ActivityStreamsPerson)
- if !ok {
- return errors.New("UPDATE: could not convert type to person")
- }
- accountable = i
- case ap.ActorService:
- l.Debug("got update for SERVICE")
- i, ok := asType.(vocab.ActivityStreamsService)
- if !ok {
- return errors.New("UPDATE: could not convert type to service")
- }
- accountable = i
- }
+ switch asType.GetTypeName() {
+ case ap.ActorApplication, ap.ActorGroup, ap.ActorOrganization, ap.ActorPerson, ap.ActorService:
+ return f.updateAccountable(ctx, receivingAccount, requestingAcct, asType)
+ }
- updatedAcct, err := f.typeConverter.ASRepresentationToAccount(ctx, accountable, "")
- if err != nil {
- return fmt.Errorf("UPDATE: error converting to account: %s", err)
- }
+ return nil
+}
- if updatedAcct.Domain == config.GetHost() || updatedAcct.Domain == config.GetAccountDomain() {
- // no need to update local accounts
- // in fact, if we do this will break the shit out of things so do NOT
- return nil
- }
+func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, asType vocab.Type) error {
+ accountable, ok := asType.(ap.Accountable)
+ if !ok {
+ return errors.New("updateAccountable: could not convert vocab.Type to Accountable")
+ }
- if requestingAcct.URI != updatedAcct.URI {
- return fmt.Errorf("UPDATE: update for account %s was requested by account %s, this is not valid", updatedAcct.URI, requestingAcct.URI)
- }
+ updatedAcct, err := f.typeConverter.ASRepresentationToAccount(ctx, accountable, "")
+ if err != nil {
+ return fmt.Errorf("updateAccountable: error converting to account: %w", err)
+ }
+
+ if updatedAcct.Domain == config.GetHost() || updatedAcct.Domain == config.GetAccountDomain() {
+ // No need to update local accounts; in fact, if we try
+ // this it will break the shit out of things so do NOT.
+ return nil
+ }
- // set some fields here on the updatedAccount representation so we don't run into db issues
- updatedAcct.CreatedAt = requestingAcct.CreatedAt
- updatedAcct.ID = requestingAcct.ID
- updatedAcct.Language = requestingAcct.Language
-
- // pass to the processor for further updating of eg., avatar/header, emojis
- // the actual db insert/update will take place a bit later
- f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
- APObjectType: ap.ObjectProfile,
- APActivityType: ap.ActivityUpdate,
- GTSModel: updatedAcct,
- ReceivingAccount: receivingAccount,
- })
+ if requestingAcct.URI != updatedAcct.URI {
+ return fmt.Errorf("updateAccountable: update for account %s was requested by account %s, this is not valid", updatedAcct.URI, requestingAcct.URI)
}
+ // Set some basic fields on the updated account
+ // based on what we already know about the requester.
+ updatedAcct.CreatedAt = requestingAcct.CreatedAt
+ updatedAcct.ID = requestingAcct.ID
+ updatedAcct.Language = requestingAcct.Language
+ updatedAcct.AvatarMediaAttachmentID = requestingAcct.AvatarMediaAttachmentID
+ updatedAcct.HeaderMediaAttachmentID = requestingAcct.HeaderMediaAttachmentID
+
+ // Pass to the processor for further updating of eg., avatar/header,
+ // emojis, etc. The actual db insert/update will take place there.
+ f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
+ APObjectType: ap.ObjectProfile,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: updatedAcct,
+ APObjectModel: accountable,
+ ReceivingAccount: receivingAcct,
+ })
+
return nil
}
diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go
index 5137145f2..8e9f67c59 100644
--- a/internal/federation/federatingdb/util.go
+++ b/internal/federation/federatingdb/util.go
@@ -325,7 +325,7 @@ func extractFromCtx(ctx context.Context) (receivingAccount, requestingAccount *g
}
func marshalItem(item vocab.Type) (string, error) {
- m, err := streams.Serialize(item)
+ m, err := ap.Serialize(item)
if err != nil {
return "", err
}
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index 7995faa84..e81a409f0 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -211,7 +211,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
// dereference the remote account (or just get it
// from the db if we already have it).
requestingAccount, err := f.GetAccountByURI(
- gtscontext.SetFastFail(ctx), username, publicKeyOwnerURI, false,
+ gtscontext.SetFastFail(ctx), username, publicKeyOwnerURI,
)
if err != nil {
if gtserror.StatusCode(err) == http.StatusGone {
diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go
index 2cf7dc9f7..bae50a749 100644
--- a/internal/gtsmodel/account.go
+++ b/internal/gtsmodel/account.go
@@ -47,7 +47,8 @@ type Account struct {
DisplayName string `validate:"-" bun:""` // DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
EmojiIDs []string `validate:"dive,ulid" bun:"emojis,array"` // Database IDs of any emojis used in this account's bio, display name, etc
Emojis []*Emoji `validate:"-" bun:"attached_emojis,m2m:account_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation
- Fields []Field `validate:"-"` // a key/value map of fields that this account has added to their profile
+ Fields []*Field `validate:"-"` // A slice of of fields that this account has added to their profile.
+ FieldsRaw []*Field `validate:"-"` // The raw (unparsed) content of fields that this account has added to their profile, without conversion to HTML, only available when requester = target
Note string `validate:"-" bun:""` // A note that this account has on their profile (ie., the account's bio/description of themselves)
NoteRaw string `validate:"-" bun:""` // The raw contents of .Note without conversion to HTML, only available when requester = target
Memorial *bool `validate:"-" bun:",default:false"` // Is this a memorial account, ie., has the user passed away?
diff --git a/internal/messages/messages.go b/internal/messages/messages.go
index 4b80088c2..7f9b3f37c 100644
--- a/internal/messages/messages.go
+++ b/internal/messages/messages.go
@@ -34,9 +34,10 @@ type FromClientAPI struct {
// FromFederator wraps a message that travels from the federator into the processor.
type FromFederator struct {
- APObjectType string // what is the object type of this message? eg., Note, Profile etc.
- APActivityType string // what is the activity type of this message? eg., Create, Follow etc.
- APIri *url.URL // what is the IRI ID of this activity?
- GTSModel interface{} // representation of this object if it's already been converted into our internal gts model
- ReceivingAccount *gtsmodel.Account // which account owns the inbox that this activity was posted to?
+ APObjectType string
+ APActivityType string
+ APIri *url.URL
+ APObjectModel interface{} // Optional AP model of the Object of the Activity. Should be Accountable or Statusable.
+ GTSModel interface{} // Optional GTS model of the Activity or Object.
+ ReceivingAccount *gtsmodel.Account // Local account which owns the inbox that this Activity was posted to.
}
diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go
index d0ea96ca2..c7d271b0a 100644
--- a/internal/processing/account/get.go
+++ b/internal/processing/account/get.go
@@ -96,7 +96,7 @@ func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Acco
}
a, err := p.federator.GetAccountByURI(
- gtscontext.SetFastFail(ctx), requestingAccount.Username, targetAccountURI, true,
+ gtscontext.SetFastFail(ctx), requestingAccount.Username, targetAccountURI,
)
if err == nil {
targetAccount = a
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index e88aba637..0baeebb6a 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -36,6 +36,14 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
+func (p *Processor) selectNoteFormatter(contentType string) text.FormatFunc {
+ if contentType == "text/markdown" {
+ return p.formatter.FromMarkdown
+ }
+
+ return p.formatter.FromPlain
+}
+
// Update processes the update of an account with the given form.
func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) {
if form.Discoverable != nil {
@@ -46,56 +54,144 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
account.Bot = form.Bot
}
- reparseEmojis := false
+ // Via the process of updating the account,
+ // it is possible that the emojis used by
+ // that account in note/display name/fields
+ // may change; we need to keep track of this.
+ var emojisChanged bool
if form.DisplayName != nil {
- if err := validate.DisplayName(*form.DisplayName); err != nil {
- return nil, gtserror.NewErrorBadRequest(err)
+ displayName := *form.DisplayName
+ if err := validate.DisplayName(displayName); err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- account.DisplayName = text.SanitizePlaintext(*form.DisplayName)
- reparseEmojis = true
+
+ // Parse new display name (always from plaintext).
+ account.DisplayName = text.SanitizePlaintext(displayName)
+
+ // If display name has changed, account emojis may have also changed.
+ emojisChanged = true
}
if form.Note != nil {
- if err := validate.Note(*form.Note); err != nil {
- return nil, gtserror.NewErrorBadRequest(err)
+ note := *form.Note
+ if err := validate.Note(note); err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
- // Set the raw note before processing
- account.NoteRaw = *form.Note
- reparseEmojis = true
+ // Store raw version of the note for now,
+ // we'll process the proper version later.
+ account.NoteRaw = note
+
+ // If note has changed, account emojis may have also changed.
+ emojisChanged = true
}
- if reparseEmojis {
- // If either DisplayName or Note changed, reparse both, because we
- // can't otherwise tell which one each emoji belongs to.
- // Deduplicate emojis between the two fields.
+ if form.FieldsAttributes != nil {
+ var (
+ fieldsAttributes = *form.FieldsAttributes
+ fieldsLen = len(fieldsAttributes)
+ fieldsRaw = make([]*gtsmodel.Field, 0, fieldsLen)
+ )
+
+ for _, updateField := range fieldsAttributes {
+ if updateField.Name == nil || updateField.Value == nil {
+ continue
+ }
+
+ var (
+ name string = *updateField.Name
+ value string = *updateField.Value
+ )
+
+ if name == "" || value == "" {
+ continue
+ }
+
+ // Sanitize raw field values.
+ fieldRaw := &gtsmodel.Field{
+ Name: text.SanitizePlaintext(name),
+ Value: text.SanitizePlaintext(value),
+ }
+ fieldsRaw = append(fieldsRaw, fieldRaw)
+ }
+
+ // Check length of parsed raw fields.
+ if err := validate.ProfileFields(fieldsRaw); err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ // OK, new raw fields are valid.
+ account.FieldsRaw = fieldsRaw
+ account.Fields = make([]*gtsmodel.Field, 0, fieldsLen) // process these in a sec
+
+ // If fields have changed, account emojis may also have changed.
+ emojisChanged = true
+ }
+
+ if emojisChanged {
+ // Use map to deduplicate emojis by their ID.
emojis := make(map[string]*gtsmodel.Emoji)
- formatResult := p.formatter.FromPlainEmojiOnly(ctx, p.parseMention, account.ID, "", account.DisplayName)
- for _, emoji := range formatResult.Emojis {
+
+ // Retrieve display name emojis.
+ for _, emoji := range p.formatter.FromPlainEmojiOnly(
+ ctx,
+ p.parseMention,
+ account.ID,
+ "",
+ account.DisplayName,
+ ).Emojis {
emojis[emoji.ID] = emoji
}
- // Process note to generate a valid HTML representation
- var f text.FormatFunc
- if account.StatusContentType == "text/markdown" {
- f = p.formatter.FromMarkdown
- } else {
- f = p.formatter.FromPlain
- }
- formatted := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
+ // Format + set note according to user prefs.
+ f := p.selectNoteFormatter(account.StatusContentType)
+ formatNoteResult := f(ctx, p.parseMention, account.ID, "", account.NoteRaw)
+ account.Note = formatNoteResult.HTML
- // Set updated HTML-ified note
- account.Note = formatted.HTML
- for _, emoji := range formatted.Emojis {
+ // Retrieve note emojis.
+ for _, emoji := range formatNoteResult.Emojis {
emojis[emoji.ID] = emoji
}
- account.Emojis = []*gtsmodel.Emoji{}
- account.EmojiIDs = []string{}
- for eid, emoji := range emojis {
+ // Process the raw fields we stored earlier.
+ for _, fieldRaw := range account.FieldsRaw {
+ field := &gtsmodel.Field{}
+
+ // Name stays plain, but we still need to
+ // see if there are any emojis set in it.
+ field.Name = fieldRaw.Name
+ for _, emoji := range p.formatter.FromPlainEmojiOnly(
+ ctx,
+ p.parseMention,
+ account.ID,
+ "",
+ fieldRaw.Name,
+ ).Emojis {
+ emojis[emoji.ID] = emoji
+ }
+
+ // Value can be HTML, but we don't want
+ // to wrap the result in <p> tags.
+ fieldFormatValueResult := p.formatter.FromPlainNoParagraph(ctx, p.parseMention, account.ID, "", fieldRaw.Value)
+ field.Value = fieldFormatValueResult.HTML
+
+ // Retrieve field emojis.
+ for _, emoji := range fieldFormatValueResult.Emojis {
+ emojis[emoji.ID] = emoji
+ }
+
+ // We're done, append the shiny new field.
+ account.Fields = append(account.Fields, field)
+ }
+
+ emojisCount := len(emojis)
+ account.Emojis = make([]*gtsmodel.Emoji, 0, emojisCount)
+ account.EmojiIDs = make([]string, 0, emojisCount)
+
+ for id, emoji := range emojis {
account.Emojis = append(account.Emojis, emoji)
- account.EmojiIDs = append(account.EmojiIDs, eid)
+ account.EmojiIDs = append(account.EmojiIDs, id)
}
}
@@ -164,26 +260,6 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
account.EnableRSS = form.EnableRSS
}
- if form.FieldsAttributes != nil && len(*form.FieldsAttributes) != 0 {
- if err := validate.ProfileFieldsCount(*form.FieldsAttributes); err != nil {
- return nil, gtserror.NewErrorBadRequest(err)
- }
-
- account.Fields = make([]gtsmodel.Field, 0) // reset fields
- for _, f := range *form.FieldsAttributes {
- if f.Name != nil && f.Value != nil {
- if *f.Name != "" && *f.Value != "" {
- field := gtsmodel.Field{}
-
- field.Name = validate.ProfileField(f.Name)
- field.Value = validate.ProfileField(f.Value)
-
- account.Fields = append(account.Fields, field)
- }
- }
- }
- }
-
err := p.state.DB.UpdateAccount(ctx, account)
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account %s: %s", account.ID, err))
diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go
index b8c245657..03a3d8703 100644
--- a/internal/processing/account/update_test.go
+++ b/internal/processing/account/update_test.go
@@ -148,6 +148,86 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
suite.Equal(expectedNote, dbAccount.Note)
}
+func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
+ testAccount := suite.testAccounts["local_account_1"]
+
+ updateFields := []apimodel.UpdateField{
+ {
+ Name: func() *string { s := "favourite emoji"; return &s }(),
+ Value: func() *string { s := ":rainbow:"; return &s }(),
+ },
+ {
+ Name: func() *string { s := "my website"; return &s }(),
+ Value: func() *string { s := "https://example.org"; return &s }(),
+ },
+ }
+
+ form := &apimodel.UpdateCredentialsRequest{
+ FieldsAttributes: &updateFields,
+ }
+
+ // should get no error from the update function, and an api model account returned
+ apiAccount, errWithCode := suite.accountProcessor.Update(context.Background(), testAccount, form)
+
+ // reset test account to avoid breaking other tests
+ testAccount.StatusContentType = "text/plain"
+ suite.NoError(errWithCode)
+ suite.NotNil(apiAccount)
+ suite.EqualValues([]apimodel.Field{
+ {
+ Name: "favourite emoji",
+ Value: ":rainbow:",
+ VerifiedAt: (*string)(nil),
+ },
+ {
+ Name: "my website",
+ Value: "<a href=\"https://example.org\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://example.org</a>",
+ VerifiedAt: (*string)(nil),
+ },
+ }, apiAccount.Fields)
+ suite.EqualValues([]apimodel.Field{
+ {
+ Name: "favourite emoji",
+ Value: ":rainbow:",
+ VerifiedAt: (*string)(nil),
+ },
+ {
+ Name: "my website",
+ Value: "https://example.org",
+ VerifiedAt: (*string)(nil),
+ },
+ }, apiAccount.Source.Fields)
+ suite.EqualValues([]apimodel.Emoji{
+ {
+ Shortcode: "rainbow",
+ URL: "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
+ StaticURL: "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
+ VisibleInPicker: true,
+ Category: "reactions",
+ },
+ }, apiAccount.Emojis)
+
+ // we should have an update in the client api channel
+ msg := <-suite.fromClientAPIChan
+ suite.Equal(ap.ActivityUpdate, msg.APActivityType)
+ suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.NotNil(msg.OriginAccount)
+ suite.Equal(testAccount.ID, msg.OriginAccount.ID)
+ suite.Nil(msg.TargetAccount)
+
+ // fields should be updated in the database as well
+ dbAccount, err := suite.db.GetAccountByID(context.Background(), testAccount.ID)
+ suite.NoError(err)
+ suite.Equal("favourite emoji", dbAccount.Fields[0].Name)
+ suite.Equal(":rainbow:", dbAccount.Fields[0].Value)
+ suite.Equal("my website", dbAccount.Fields[1].Name)
+ suite.Equal("<a href=\"https://example.org\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">https://example.org</a>", dbAccount.Fields[1].Value)
+ suite.Equal("favourite emoji", dbAccount.FieldsRaw[0].Name)
+ suite.Equal(":rainbow:", dbAccount.FieldsRaw[0].Value)
+ suite.Equal("my website", dbAccount.FieldsRaw[1].Name)
+ suite.Equal("https://example.org", dbAccount.FieldsRaw[1].Value)
+}
+
func TestAccountUpdateTestSuite(t *testing.T) {
suite.Run(t, new(AccountUpdateTestSuite))
}
diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go
index 00367b65d..a56b001b7 100644
--- a/internal/processing/fedi/collections.go
+++ b/internal/processing/fedi/collections.go
@@ -24,7 +24,6 @@ import (
"net/http"
"net/url"
- "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
@@ -74,7 +73,7 @@ func (p *Processor) OutboxGet(ctx context.Context, requestedUsername string, pag
return nil, gtserror.NewErrorInternalError(err)
}
- data, err = streams.Serialize(collection)
+ data, err = ap.Serialize(collection)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -93,7 +92,7 @@ func (p *Processor) OutboxGet(ctx context.Context, requestedUsername string, pag
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- data, err = streams.Serialize(outboxPage)
+ data, err = ap.Serialize(outboxPage)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -119,7 +118,7 @@ func (p *Processor) FollowersGet(ctx context.Context, requestedUsername string)
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
}
- data, err := streams.Serialize(requestedFollowers)
+ data, err := ap.Serialize(requestedFollowers)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -145,7 +144,7 @@ func (p *Processor) FollowingGet(ctx context.Context, requestedUsername string)
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
}
- data, err := streams.Serialize(requestedFollowing)
+ data, err := ap.Serialize(requestedFollowing)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -173,7 +172,7 @@ func (p *Processor) FeaturedCollectionGet(ctx context.Context, requestedUsername
return nil, gtserror.NewErrorInternalError(err)
}
- data, err := ap.SerializeOrderedCollection(collection)
+ data, err := ap.Serialize(collection)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/common.go b/internal/processing/fedi/common.go
index 3fade397b..093a9d761 100644
--- a/internal/processing/fedi/common.go
+++ b/internal/processing/fedi/common.go
@@ -40,7 +40,7 @@ func (p *Processor) authenticate(ctx context.Context, requestedUsername string)
return
}
- if requestingAccount, err = p.federator.GetAccountByURI(gtscontext.SetFastFail(ctx), requestedUsername, requestingAccountURI, false); err != nil {
+ if requestingAccount, err = p.federator.GetAccountByURI(gtscontext.SetFastFail(ctx), requestedUsername, requestingAccountURI); err != nil {
errWithCode = gtserror.NewErrorUnauthorized(err)
return
}
diff --git a/internal/processing/fedi/emoji.go b/internal/processing/fedi/emoji.go
index d1a035668..ea7cb6082 100644
--- a/internal/processing/fedi/emoji.go
+++ b/internal/processing/fedi/emoji.go
@@ -21,7 +21,7 @@ import (
"context"
"fmt"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
@@ -49,7 +49,7 @@ func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string) (inte
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err))
}
- data, err := streams.Serialize(apEmoji)
+ data, err := ap.Serialize(apEmoji)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go
index 072ff6aaf..a9b3e1eee 100644
--- a/internal/processing/fedi/status.go
+++ b/internal/processing/fedi/status.go
@@ -22,7 +22,7 @@ import (
"fmt"
"net/url"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
@@ -57,7 +57,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUsername string, req
return nil, gtserror.NewErrorInternalError(err)
}
- data, err := streams.Serialize(asStatus)
+ data, err := ap.Serialize(asStatus)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -105,7 +105,7 @@ func (p *Processor) StatusRepliesGet(ctx context.Context, requestedUsername stri
return nil, gtserror.NewErrorInternalError(err)
}
- data, err = streams.Serialize(collection)
+ data, err = ap.Serialize(collection)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -117,7 +117,7 @@ func (p *Processor) StatusRepliesGet(ctx context.Context, requestedUsername stri
return nil, gtserror.NewErrorInternalError(err)
}
// but only return the first page
- data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage())
+ data, err = ap.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage())
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
@@ -166,7 +166,7 @@ func (p *Processor) StatusRepliesGet(ctx context.Context, requestedUsername stri
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
- data, err = streams.Serialize(repliesPage)
+ data, err = ap.Serialize(repliesPage)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go
index 28dc3c857..b78f6de9d 100644
--- a/internal/processing/fedi/user.go
+++ b/internal/processing/fedi/user.go
@@ -22,8 +22,8 @@ import (
"fmt"
"net/url"
- "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@@ -56,7 +56,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
// if we're not already handshaking/dereferencing a remote account, dereference it now
if !p.federator.Handshaking(requestedUsername, requestingAccountURI) {
requestingAccount, err := p.federator.GetAccountByURI(
- gtscontext.SetFastFail(ctx), requestedUsername, requestingAccountURI, false,
+ gtscontext.SetFastFail(ctx), requestedUsername, requestingAccountURI,
)
if err != nil {
return nil, gtserror.NewErrorUnauthorized(err)
@@ -78,7 +78,7 @@ func (p *Processor) UserGet(ctx context.Context, requestedUsername string, reque
}
}
- data, err := streams.Serialize(requestedPerson)
+ data, err := ap.Serialize(requestedPerson)
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go
index 929ef824f..82e08060c 100644
--- a/internal/processing/fromfederator.go
+++ b/internal/processing/fromfederator.go
@@ -145,7 +145,7 @@ func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federa
status.Account = a
}
- // do a BLOCKING get of the remote account to make sure the avi and header are cached
+ // Get the remote account to make sure the avi and header are cached.
if status.Account.Domain != "" {
remoteAccountID, err := url.Parse(status.Account.URI)
if err != nil {
@@ -155,7 +155,6 @@ func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federa
a, err := p.federator.GetAccountByURI(ctx,
federatorMsg.ReceivingAccount.Username,
remoteAccountID,
- true,
)
if err != nil {
return err
@@ -187,7 +186,7 @@ func (p *Processor) processCreateFaveFromFederator(ctx context.Context, federato
incomingFave.Account = a
}
- // do a BLOCKING get of the remote account to make sure the avi and header are cached
+ // Get the remote account to make sure the avi and header are cached.
if incomingFave.Account.Domain != "" {
remoteAccountID, err := url.Parse(incomingFave.Account.URI)
if err != nil {
@@ -197,7 +196,6 @@ func (p *Processor) processCreateFaveFromFederator(ctx context.Context, federato
a, err := p.federator.GetAccountByURI(ctx,
federatorMsg.ReceivingAccount.Username,
remoteAccountID,
- true,
)
if err != nil {
return err
@@ -229,7 +227,7 @@ func (p *Processor) processCreateFollowRequestFromFederator(ctx context.Context,
followRequest.Account = a
}
- // do a BLOCKING get of the remote account to make sure the avi and header are cached
+ // Get the remote account to make sure the avi and header are cached.
if followRequest.Account.Domain != "" {
remoteAccountID, err := url.Parse(followRequest.Account.URI)
if err != nil {
@@ -239,7 +237,6 @@ func (p *Processor) processCreateFollowRequestFromFederator(ctx context.Context,
a, err := p.federator.GetAccountByURI(ctx,
federatorMsg.ReceivingAccount.Username,
remoteAccountID,
- true,
)
if err != nil {
return err
@@ -290,7 +287,7 @@ func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, fede
incomingAnnounce.Account = a
}
- // do a BLOCKING get of the remote account to make sure the avi and header are cached
+ // Get the remote account to make sure the avi and header are cached.
if incomingAnnounce.Account.Domain != "" {
remoteAccountID, err := url.Parse(incomingAnnounce.Account.URI)
if err != nil {
@@ -300,7 +297,6 @@ func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, fede
a, err := p.federator.GetAccountByURI(ctx,
federatorMsg.ReceivingAccount.Username,
remoteAccountID,
- true,
)
if err != nil {
return err
@@ -370,16 +366,28 @@ func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federato
func (p *Processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error {
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
if !ok {
- return errors.New("profile was not parseable as *gtsmodel.Account")
+ return errors.New("*gtsmodel.Account was not parseable on update account message")
}
- // Call UpdateAccount with force to reflect that
- // we want to fetch new bio, avatar, header, etc.
- if _, err := p.federator.UpdateAccount(ctx,
+ // Because this was an Update, the new AP Object should be set on the message.
+ incomingAccountable, ok := federatorMsg.APObjectModel.(ap.Accountable)
+ if !ok {
+ return errors.New("Accountable was not parseable on update account message")
+ }
+
+ // Call RefreshAccount to fetch up-to-date bio, avatar, header, etc.
+ updatedAccount, err := p.federator.RefreshAccount(
+ ctx,
federatorMsg.ReceivingAccount.Username,
+ incomingAccountable,
incomingAccount,
- true,
- ); err != nil {
+ )
+ if err != nil {
+ return fmt.Errorf("error enriching updated account from federator: %s", err)
+ }
+
+ // RefreshAccount doesn't make DB update calls, so do that here.
+ if err := p.state.DB.UpdateAccount(ctx, updatedAccount); err != nil {
return fmt.Errorf("error enriching updated account from federator: %s", err)
}
diff --git a/internal/processing/search.go b/internal/processing/search.go
index 624537b6a..c8dc58320 100644
--- a/internal/processing/search.go
+++ b/internal/processing/search.go
@@ -270,7 +270,7 @@ func (p *Processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth,
return p.federator.GetAccountByURI(
gtscontext.SetFastFail(ctx),
authed.Account.Username,
- uri, false,
+ uri,
)
}
@@ -297,6 +297,6 @@ func (p *Processor) searchAccountByUsernameDomain(ctx context.Context, authed *o
return p.federator.GetAccountByUsernameDomain(
gtscontext.SetFastFail(ctx),
authed.Account.Username,
- username, domain, false,
+ username, domain,
)
}
diff --git a/internal/processing/util.go b/internal/processing/util.go
index 967c03f9f..601d8c4de 100644
--- a/internal/processing/util.go
+++ b/internal/processing/util.go
@@ -62,7 +62,6 @@ func GetParseMentionFunc(dbConn db.DB, federator federation.Federator) gtsmodel.
requestingUsername,
username,
domain,
- false,
)
if err != nil {
return nil, fmt.Errorf("parseMentionFunc: error fetching account: %s", err)
diff --git a/internal/text/formatter.go b/internal/text/formatter.go
index 212d2b58d..0e5e0b554 100644
--- a/internal/text/formatter.go
+++ b/internal/text/formatter.go
@@ -30,6 +30,8 @@ import (
type Formatter interface {
// FromPlain parses an HTML text from a plaintext.
FromPlain(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, plain string) *FormatResult
+ // FromPlainNoParagraph parses an HTML text from a plaintext, without wrapping the resulting text in <p> tags.
+ FromPlainNoParagraph(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, plain string) *FormatResult
// FromMarkdown parses an HTML text from a markdown-formatted text.
FromMarkdown(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, md string) *FormatResult
// FromPlainEmojiOnly parses an HTML text from a plaintext, only parsing emojis and not mentions etc.
diff --git a/internal/text/formatter_test.go b/internal/text/formatter_test.go
index 8498a6408..403ba8e8e 100644
--- a/internal/text/formatter_test.go
+++ b/internal/text/formatter_test.go
@@ -92,3 +92,7 @@ func (suite *TextStandardTestSuite) FromMarkdown(text string) *text.FormatResult
func (suite *TextStandardTestSuite) FromPlain(text string) *text.FormatResult {
return suite.formatter.FromPlain(context.Background(), suite.parseMention, suite.testAccounts["local_account_1"].ID, "status_ID", text)
}
+
+func (suite *TextStandardTestSuite) FromPlainNoParagraph(text string) *text.FormatResult {
+ return suite.formatter.FromPlainNoParagraph(context.Background(), suite.parseMention, suite.testAccounts["local_account_1"].ID, "status_ID", text)
+}
diff --git a/internal/text/goldmark_plaintext.go b/internal/text/goldmark_plaintext.go
index 6f841920c..635fdfc33 100644
--- a/internal/text/goldmark_plaintext.go
+++ b/internal/text/goldmark_plaintext.go
@@ -60,3 +60,41 @@ func (b *plaintextParser) CanInterruptParagraph() bool {
func (b *plaintextParser) CanAcceptIndentedLine() bool {
return true
}
+
+// plaintextParserNoParagraph implements goldmark.parser.BlockParser
+type plaintextParserNoParagraph struct{}
+
+var defaultPlaintextParserNoParagraph = &plaintextParserNoParagraph{}
+
+func newPlaintextParserNoParagraph() parser.BlockParser {
+ return defaultPlaintextParserNoParagraph
+}
+
+func (b *plaintextParserNoParagraph) Trigger() []byte {
+ return nil
+}
+
+func (b *plaintextParserNoParagraph) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
+ _, segment := reader.PeekLine()
+ node := ast.NewDocument()
+ node.Lines().Append(segment)
+ reader.Advance(segment.Len() - 1)
+ return node, parser.NoChildren
+}
+
+func (b *plaintextParserNoParagraph) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
+ _, segment := reader.PeekLine()
+ node.Lines().Append(segment)
+ reader.Advance(segment.Len() - 1)
+ return parser.Continue | parser.NoChildren
+}
+
+func (b *plaintextParserNoParagraph) Close(node ast.Node, reader text.Reader, pc parser.Context) {}
+
+func (b *plaintextParserNoParagraph) CanInterruptParagraph() bool {
+ return false
+}
+
+func (b *plaintextParserNoParagraph) CanAcceptIndentedLine() bool {
+ return true
+}
diff --git a/internal/text/plain.go b/internal/text/plain.go
index 80916dfad..b1c2a2c33 100644
--- a/internal/text/plain.go
+++ b/internal/text/plain.go
@@ -30,26 +30,28 @@ import (
"github.com/yuin/goldmark/util"
)
-func (f *formatter) FromPlain(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, plain string) *FormatResult {
+func (f *formatter) fromPlain(
+ ctx context.Context,
+ ptParser parser.Parser,
+ pmf gtsmodel.ParseMentionFunc,
+ authorID string,
+ statusID string,
+ plain string,
+) *FormatResult {
result := &FormatResult{
Mentions: []*gtsmodel.Mention{},
Tags: []*gtsmodel.Tag{},
Emojis: []*gtsmodel.Emoji{},
}
- // parse markdown text into html, using custom renderer to add hashtag/mention links
+ // Parse markdown into html, using custom renderer
+ // to add hashtag/mention links and emoji images.
md := goldmark.New(
goldmark.WithRendererOptions(
html.WithXHTML(),
html.WithHardWraps(),
),
- goldmark.WithParser(
- parser.NewParser(
- parser.WithBlockParsers(
- util.Prioritized(newPlaintextParser(), 500),
- ),
- ),
- ),
+ goldmark.WithParser(ptParser), // use parser we were passed
goldmark.WithExtensions(
&customRenderer{f, ctx, pmf, authorID, statusID, false, result},
extension.Linkify, // turns URLs into links
@@ -57,20 +59,40 @@ func (f *formatter) FromPlain(ctx context.Context, pmf gtsmodel.ParseMentionFunc
)
var htmlContentBytes bytes.Buffer
- err := md.Convert([]byte(plain), &htmlContentBytes)
- if err != nil {
+ if err := md.Convert([]byte(plain), &htmlContentBytes); err != nil {
log.Errorf(ctx, "error formatting plaintext to HTML: %s", err)
}
result.HTML = htmlContentBytes.String()
- // clean anything dangerous out of the HTML
+ // Clean anything dangerous out of resulting HTML.
result.HTML = SanitizeHTML(result.HTML)
- // shrink ray
- result.HTML, err = m.String("text/html", result.HTML)
- if err != nil {
+ // Shrink ray!
+ var err error
+ if result.HTML, err = m.String("text/html", result.HTML); err != nil {
log.Errorf(ctx, "error minifying HTML: %s", err)
}
return result
}
+
+func (f *formatter) FromPlain(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, plain string) *FormatResult {
+ ptParser := parser.NewParser(
+ parser.WithBlockParsers(
+ util.Prioritized(newPlaintextParser(), 500),
+ ),
+ )
+
+ return f.fromPlain(ctx, ptParser, pmf, authorID, statusID, plain)
+}
+
+func (f *formatter) FromPlainNoParagraph(ctx context.Context, pmf gtsmodel.ParseMentionFunc, authorID string, statusID string, plain string) *FormatResult {
+ ptParser := parser.NewParser(
+ parser.WithBlockParsers(
+ // Initialize block parser that doesn't wrap in <p> tags.
+ util.Prioritized(newPlaintextParserNoParagraph(), 500),
+ ),
+ )
+
+ return f.fromPlain(ctx, ptParser, pmf, authorID, statusID, plain)
+}
diff --git a/internal/text/plain_test.go b/internal/text/plain_test.go
index c00ae70c3..5a2918563 100644
--- a/internal/text/plain_test.go
+++ b/internal/text/plain_test.go
@@ -25,14 +25,16 @@ import (
)
const (
- simple = "this is a plain and simple status"
- simpleExpected = "<p>this is a plain and simple status</p>"
- withTag = "here's a simple status that uses hashtag #welcome!"
- withTagExpected = "<p>here's a simple status that uses hashtag <a href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>welcome</span></a>!</p>"
- withHTML = "<div>blah this should just be html escaped blah</div>"
- withHTMLExpected = "<p>&lt;div>blah this should just be html escaped blah&lt;/div></p>"
- moreComplex = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\nText\n\n:rainbow:"
- moreComplexExpected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br>Text<br><br>:rainbow:</p>"
+ simple = "this is a plain and simple status"
+ simpleExpected = "<p>this is a plain and simple status</p>"
+ simpleExpectedNoParagraph = "this is a plain and simple status"
+ withTag = "here's a simple status that uses hashtag #welcome!"
+ withTagExpected = "<p>here's a simple status that uses hashtag <a href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>welcome</span></a>!</p>"
+ withTagExpectedNoParagraph = "here's a simple status that uses hashtag <a href=\"http://localhost:8080/tags/welcome\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>welcome</span></a>!"
+ withHTML = "<div>blah this should just be html escaped blah</div>"
+ withHTMLExpected = "<p>&lt;div>blah this should just be html escaped blah&lt;/div></p>"
+ moreComplex = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\nText\n\n:rainbow:"
+ moreComplexExpected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br>Text<br><br>:rainbow:</p>"
)
type PlainTestSuite struct {
@@ -44,11 +46,21 @@ func (suite *PlainTestSuite) TestParseSimple() {
suite.Equal(simpleExpected, formatted.HTML)
}
+func (suite *PlainTestSuite) TestParseSimpleNoParagraph() {
+ formatted := suite.FromPlainNoParagraph(simple)
+ suite.Equal(simpleExpectedNoParagraph, formatted.HTML)
+}
+
func (suite *PlainTestSuite) TestParseWithTag() {
formatted := suite.FromPlain(withTag)
suite.Equal(withTagExpected, formatted.HTML)
}
+func (suite *PlainTestSuite) TestParseWithTagNoParagraph() {
+ formatted := suite.FromPlainNoParagraph(withTag)
+ suite.Equal(withTagExpectedNoParagraph, formatted.HTML)
+}
+
func (suite *PlainTestSuite) TestParseWithHTML() {
formatted := suite.FromPlain(withHTML)
suite.Equal(withHTMLExpected, formatted.HTML)
diff --git a/internal/transport/controller.go b/internal/transport/controller.go
index f77fbbb92..83fad8038 100644
--- a/internal/transport/controller.go
+++ b/internal/transport/controller.go
@@ -29,7 +29,7 @@ import (
"codeberg.org/gruf/go-byteutil"
"codeberg.org/gruf/go-cache/v3"
"github.com/superseriousbusiness/activity/pub"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb"
"github.com/superseriousbusiness/gotosocial/internal/httpclient"
@@ -157,7 +157,7 @@ func (c *controller) dereferenceLocalFollowers(ctx context.Context, iri *url.URL
return nil, err
}
- i, err := streams.Serialize(followers)
+ i, err := ap.Serialize(followers)
if err != nil {
return nil, err
}
@@ -175,7 +175,7 @@ func (c *controller) dereferenceLocalUser(ctx context.Context, iri *url.URL) ([]
return nil, err
}
- i, err := streams.Serialize(user)
+ i, err := ap.Serialize(user)
if err != nil {
return nil, err
}
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
index f94ac128a..7c0b60ad5 100644
--- a/internal/typeutils/astointernal.go
+++ b/internal/typeutils/astointernal.go
@@ -86,7 +86,8 @@ func (c *converter) ASRepresentationToAccount(ctx context.Context, accountable a
acct.Emojis = emojis
}
- // TODO: fields aka attachment array
+ // fields aka attachment array
+ acct.Fields = ap.ExtractFields(accountable)
// note aka summary
acct.Note = ap.ExtractSummary(accountable)
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index ceff1cb4d..3c1615cfb 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -240,7 +240,25 @@ func (c *converter) AccountToAS(ctx context.Context, a *gtsmodel.Account) (vocab
// attachment
// Used for profile fields.
- // TODO: The PropertyValue type has to be added: https://schema.org/PropertyValue
+ if len(a.Fields) != 0 {
+ attachmentProp := streams.NewActivityStreamsAttachmentProperty()
+
+ for _, field := range a.Fields {
+ propertyValue := streams.NewSchemaPropertyValue()
+
+ nameProp := streams.NewActivityStreamsNameProperty()
+ nameProp.AppendXMLSchemaString(field.Name)
+ propertyValue.SetActivityStreamsName(nameProp)
+
+ valueProp := streams.NewSchemaValueProperty()
+ valueProp.Set(field.Value)
+ propertyValue.SetSchemaValue(valueProp)
+
+ attachmentProp.AppendSchemaPropertyValue(propertyValue)
+ }
+
+ person.SetActivityStreamsAttachment(attachmentProp)
+ }
// endpoints
// NOT IMPLEMENTED -- this is for shared inbox which we don't use
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 486866160..60c59326c 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -21,11 +21,11 @@ import (
"context"
"encoding/json"
"errors"
+ "fmt"
"strings"
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@@ -43,7 +43,7 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
suite.NoError(err)
- ser, err := streams.Serialize(asPerson)
+ ser, err := ap.Serialize(asPerson)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -85,6 +85,107 @@ func (suite *InternalToASTestSuite) TestAccountToAS() {
}`, trimmed)
}
+func (suite *InternalToASTestSuite) TestAccountToASWithFields() {
+ testAccount := &gtsmodel.Account{}
+ *testAccount = *suite.testAccounts["local_account_2"]
+
+ asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
+ suite.NoError(err)
+
+ ser, err := ap.Serialize(asPerson)
+ suite.NoError(err)
+
+ bytes, err := json.MarshalIndent(ser, "", " ")
+ suite.NoError(err)
+
+ // trim off everything up to 'attachment';
+ // this is necessary because the order of multiple 'context' entries is not determinate
+ trimmed := strings.Split(string(bytes), "\"attachment\"")[1]
+
+ fmt.Printf("\n\n\n%s\n\n\n", string(bytes))
+
+ suite.Equal(`: [
+ {
+ "name": "should you follow me?",
+ "type": "PropertyValue",
+ "value": "maybe!"
+ },
+ {
+ "name": "age",
+ "type": "PropertyValue",
+ "value": "120"
+ }
+ ],
+ "discoverable": false,
+ "featured": "http://localhost:8080/users/1happyturtle/collections/featured",
+ "followers": "http://localhost:8080/users/1happyturtle/followers",
+ "following": "http://localhost:8080/users/1happyturtle/following",
+ "id": "http://localhost:8080/users/1happyturtle",
+ "inbox": "http://localhost:8080/users/1happyturtle/inbox",
+ "manuallyApprovesFollowers": true,
+ "name": "happy little turtle :3",
+ "outbox": "http://localhost:8080/users/1happyturtle/outbox",
+ "preferredUsername": "1happyturtle",
+ "publicKey": {
+ "id": "http://localhost:8080/users/1happyturtle#main-key",
+ "owner": "http://localhost:8080/users/1happyturtle",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "tag": [],
+ "type": "Person",
+ "url": "http://localhost:8080/@1happyturtle"
+}`, trimmed)
+}
+
+func (suite *InternalToASTestSuite) TestAccountToASWithOneField() {
+ testAccount := &gtsmodel.Account{}
+ *testAccount = *suite.testAccounts["local_account_2"]
+ testAccount.Fields = testAccount.Fields[0:1] // Take only one field.
+
+ asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
+ suite.NoError(err)
+
+ ser, err := ap.Serialize(asPerson)
+ suite.NoError(err)
+
+ bytes, err := json.MarshalIndent(ser, "", " ")
+ suite.NoError(err)
+
+ // trim off everything up to 'attachment';
+ // this is necessary because the order of multiple 'context' entries is not determinate
+ trimmed := strings.Split(string(bytes), "\"attachment\"")[1]
+
+ // Despite only one field being set, attachments should still be a slice/array.
+ suite.Equal(`: [
+ {
+ "name": "should you follow me?",
+ "type": "PropertyValue",
+ "value": "maybe!"
+ }
+ ],
+ "discoverable": false,
+ "featured": "http://localhost:8080/users/1happyturtle/collections/featured",
+ "followers": "http://localhost:8080/users/1happyturtle/followers",
+ "following": "http://localhost:8080/users/1happyturtle/following",
+ "id": "http://localhost:8080/users/1happyturtle",
+ "inbox": "http://localhost:8080/users/1happyturtle/inbox",
+ "manuallyApprovesFollowers": true,
+ "name": "happy little turtle :3",
+ "outbox": "http://localhost:8080/users/1happyturtle/outbox",
+ "preferredUsername": "1happyturtle",
+ "publicKey": {
+ "id": "http://localhost:8080/users/1happyturtle#main-key",
+ "owner": "http://localhost:8080/users/1happyturtle",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtTc6Jpg6LrRPhVQG4KLz\n2+YqEUUtZPd4YR+TKXuCnwEG9ZNGhgP046xa9h3EWzrZXaOhXvkUQgJuRqPrAcfN\nvc8jBHV2xrUeD8pu/MWKEabAsA/tgCv3nUC47HQ3/c12aHfYoPz3ufWsGGnrkhci\nv8PaveJ3LohO5vjCn1yZ00v6osMJMViEZvZQaazyE9A8FwraIexXabDpoy7tkHRg\nA1fvSkg4FeSG1XMcIz2NN7xyUuFACD+XkuOk7UqzRd4cjPUPLxiDwIsTlcgGOd3E\nUFMWVlPxSGjY2hIKa3lEHytaYK9IMYdSuyCsJshd3/yYC9LqxZY2KdlKJ80VOVyh\nyQIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "summary": "\u003cp\u003ei post about things that concern me\u003c/p\u003e",
+ "tag": [],
+ "type": "Person",
+ "url": "http://localhost:8080/@1happyturtle"
+}`, trimmed)
+}
+
func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"] // take zork for this test
@@ -93,7 +194,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithEmoji() {
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
suite.NoError(err)
- ser, err := streams.Serialize(asPerson)
+ ser, err := ap.Serialize(asPerson)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -154,7 +255,7 @@ func (suite *InternalToASTestSuite) TestAccountToASWithSharedInbox() {
asPerson, err := suite.typeconverter.AccountToAS(context.Background(), testAccount)
suite.NoError(err)
- ser, err := streams.Serialize(asPerson)
+ ser, err := ap.Serialize(asPerson)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -206,7 +307,7 @@ func (suite *InternalToASTestSuite) TestOutboxToASCollection() {
collection, err := suite.typeconverter.OutboxToASCollection(ctx, testAccount.OutboxURI)
suite.NoError(err)
- ser, err := streams.Serialize(collection)
+ ser, err := ap.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -227,7 +328,7 @@ func (suite *InternalToASTestSuite) TestStatusToAS() {
asStatus, err := suite.typeconverter.StatusToAS(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asStatus)
+ ser, err := ap.Serialize(asStatus)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -268,7 +369,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASWithIDs() {
asStatus, err := suite.typeconverter.StatusToAS(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asStatus)
+ ser, err := ap.Serialize(asStatus)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -328,7 +429,7 @@ func (suite *InternalToASTestSuite) TestStatusWithTagsToASFromDB() {
asStatus, err := suite.typeconverter.StatusToAS(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asStatus)
+ ser, err := ap.Serialize(asStatus)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -389,7 +490,7 @@ func (suite *InternalToASTestSuite) TestStatusToASWithMentions() {
asStatus, err := suite.typeconverter.StatusToAS(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asStatus)
+ ser, err := ap.Serialize(asStatus)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -437,7 +538,7 @@ func (suite *InternalToASTestSuite) TestStatusToASDeletePublicReply() {
asDelete, err := suite.typeconverter.StatusToASDelete(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asDelete)
+ ser, err := ap.Serialize(asDelete)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -475,7 +576,7 @@ func (suite *InternalToASTestSuite) TestStatusToASDeletePublicReplyOriginalDelet
asDelete, err := suite.typeconverter.StatusToASDelete(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asDelete)
+ ser, err := ap.Serialize(asDelete)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -501,7 +602,7 @@ func (suite *InternalToASTestSuite) TestStatusToASDeletePublic() {
asDelete, err := suite.typeconverter.StatusToASDelete(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asDelete)
+ ser, err := ap.Serialize(asDelete)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -524,7 +625,7 @@ func (suite *InternalToASTestSuite) TestStatusToASDeleteDirectMessage() {
asDelete, err := suite.typeconverter.StatusToASDelete(ctx, testStatus)
suite.NoError(err)
- ser, err := streams.Serialize(asDelete)
+ ser, err := ap.Serialize(asDelete)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -551,7 +652,7 @@ func (suite *InternalToASTestSuite) TestStatusesToASOutboxPage() {
page, err := suite.typeconverter.StatusesToASOutboxPage(ctx, testAccount.OutboxURI, "", "", statuses)
suite.NoError(err)
- ser, err := streams.Serialize(page)
+ ser, err := ap.Serialize(page)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -604,7 +705,7 @@ func (suite *InternalToASTestSuite) TestSelfBoostFollowersOnlyToAS() {
asBoost, err := suite.typeconverter.BoostToAS(ctx, boostWrapperStatus, testAccount, testAccount)
suite.NoError(err)
- ser, err := streams.Serialize(asBoost)
+ ser, err := ap.Serialize(asBoost)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -637,7 +738,7 @@ func (suite *InternalToASTestSuite) TestReportToAS() {
flag, err := suite.typeconverter.ReportToASFlag(ctx, testReport)
suite.NoError(err)
- ser, err := streams.Serialize(flag)
+ ser, err := ap.Serialize(flag)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -670,7 +771,7 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASSomeItems() {
suite.FailNow(err.Error())
}
- ser, err := ap.SerializeOrderedCollection(collection)
+ ser, err := ap.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -702,7 +803,7 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASNoItems() {
suite.FailNow(err.Error())
}
- ser, err := ap.SerializeOrderedCollection(collection)
+ ser, err := ap.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
@@ -731,7 +832,7 @@ func (suite *InternalToASTestSuite) TestPinnedStatusesToASOneItem() {
suite.FailNow(err.Error())
}
- ser, err := ap.SerializeOrderedCollection(collection)
+ ser, err := ap.Serialize(collection)
suite.NoError(err)
bytes, err := json.MarshalIndent(ser, "", " ")
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 88646c311..1e5fb024f 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -77,7 +77,7 @@ func (c *converter) AccountToAPIAccountSensitive(ctx context.Context, a *gtsmode
Language: a.Language,
StatusContentType: statusContentType,
Note: a.NoteRaw,
- Fields: apiAccount.Fields,
+ Fields: c.fieldsToAPIFields(a.FieldsRaw),
FollowRequestsCount: frc,
}
@@ -131,7 +131,6 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
aviURLStatic string
headerURL string
headerURLStatic string
- fields = make([]apimodel.Field, len(a.Fields))
)
if a.AvatarMediaAttachment != nil {
@@ -144,19 +143,8 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
headerURLStatic = a.HeaderMediaAttachment.Thumbnail.URL
}
- // GTS model fields -> frontend.
- for i, field := range a.Fields {
- mField := apimodel.Field{
- Name: field.Name,
- Value: field.Value,
- }
-
- if !field.VerifiedAt.IsZero() {
- mField.VerifiedAt = util.FormatISO8601(field.VerifiedAt)
- }
-
- fields[i] = mField
- }
+ // convert account gts model fields to front api model fields
+ fields := c.fieldsToAPIFields(a.Fields)
// GTS model emojis -> frontend.
apiEmojis, err := c.convertEmojisToAPIEmojis(ctx, a.Emojis, a.EmojiIDs)
@@ -239,6 +227,25 @@ func (c *converter) AccountToAPIAccountPublic(ctx context.Context, a *gtsmodel.A
return accountFrontend, nil
}
+func (c *converter) fieldsToAPIFields(f []*gtsmodel.Field) []apimodel.Field {
+ fields := make([]apimodel.Field, len(f))
+
+ for i, field := range f {
+ mField := apimodel.Field{
+ Name: field.Name,
+ Value: field.Value,
+ }
+
+ if !field.VerifiedAt.IsZero() {
+ mField.VerifiedAt = func() *string { s := util.FormatISO8601(field.VerifiedAt); return &s }()
+ }
+
+ fields[i] = mField
+ }
+
+ return fields
+}
+
func (c *converter) AccountToAPIAccountBlocked(ctx context.Context, a *gtsmodel.Account) (*apimodel.Account, error) {
var (
acct string
diff --git a/internal/typeutils/internaltofrontend_test.go b/internal/typeutils/internaltofrontend_test.go
index c8ab5a8e1..558d3acea 100644
--- a/internal/typeutils/internaltofrontend_test.go
+++ b/internal/typeutils/internaltofrontend_test.go
@@ -873,7 +873,18 @@ func (suite *InternalToFrontendTestSuite) TestReportToFrontend2() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
@@ -977,7 +988,18 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend1() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
@@ -1137,7 +1159,18 @@ func (suite *InternalToFrontendTestSuite) TestAdminReportToFrontend2() {
"statuses_count": 7,
"last_status_at": "2021-10-20T10:40:37.000Z",
"emojis": [],
- "fields": [],
+ "fields": [
+ {
+ "name": "should you follow me?",
+ "value": "maybe!",
+ "verified_at": null
+ },
+ {
+ "name": "age",
+ "value": "120",
+ "verified_at": null
+ }
+ ],
"role": {
"name": "user"
}
diff --git a/internal/typeutils/wrap_test.go b/internal/typeutils/wrap_test.go
index 2978a0251..46d28f5c4 100644
--- a/internal/typeutils/wrap_test.go
+++ b/internal/typeutils/wrap_test.go
@@ -23,7 +23,7 @@ import (
"testing"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/activity/streams"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
)
type WrapTestSuite struct {
@@ -40,7 +40,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreateIRIOnly() {
suite.NoError(err)
suite.NotNil(create)
- createI, err := streams.Serialize(create)
+ createI, err := ap.Serialize(create)
suite.NoError(err)
bytes, err := json.MarshalIndent(createI, "", " ")
@@ -68,7 +68,7 @@ func (suite *WrapTestSuite) TestWrapNoteInCreate() {
suite.NoError(err)
suite.NotNil(create)
- createI, err := streams.Serialize(create)
+ createI, err := ap.Serialize(create)
suite.NoError(err)
bytes, err := json.MarshalIndent(createI, "", " ")
diff --git a/internal/validate/account_test.go b/internal/validate/account_test.go
index 5a7855492..57f0f4900 100644
--- a/internal/validate/account_test.go
+++ b/internal/validate/account_test.go
@@ -50,7 +50,7 @@ func happyAccount() *gtsmodel.Account {
HeaderMediaAttachment: nil,
HeaderRemoteURL: "",
DisplayName: "original zork (he/they)",
- Fields: []gtsmodel.Field{},
+ Fields: []*gtsmodel.Field{},
Note: "hey yo this is my profile!",
Memorial: testrig.FalseBool(),
AlsoKnownAs: "",
diff --git a/internal/validate/formvalidation.go b/internal/validate/formvalidation.go
index dd3925572..20d4aa782 100644
--- a/internal/validate/formvalidation.go
+++ b/internal/validate/formvalidation.go
@@ -25,6 +25,7 @@ import (
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/regexes"
pwv "github.com/wagslane/go-password-validator"
"golang.org/x/text/language"
@@ -43,7 +44,7 @@ const (
maximumCustomCSSLength = 5000
maximumEmojiCategoryLength = 64
maximumProfileFieldLength = 255
- maximumProfileFields = 4
+ maximumProfileFields = 6
)
// NewPassword returns an error if the given password is not sufficiently strong, or nil if it's ok.
@@ -233,19 +234,26 @@ func ULID(i string) bool {
return regexes.ULID.MatchString(i)
}
-func ProfileFieldsCount(fields []apimodel.UpdateField) error {
- if length := len(fields); length > maximumProfileFields {
+// ProfileFields validates the length of provided fields slice,
+// and also iterates through the fields and trims each name + value
+// to maximumProfileFieldLength, if they were above.
+func ProfileFields(fields []*gtsmodel.Field) error {
+ if len(fields) > maximumProfileFields {
return fmt.Errorf("cannot have more than %d profile fields", maximumProfileFields)
}
- return nil
-}
-
-func ProfileField(f *string) string {
- s := []rune(*f)
- if len(s) > maximumProfileFieldLength {
- return string(s[:maximumProfileFieldLength]) // trim profile field to maximum allowed length
+ // Trim each field name + value to maximum allowed length.
+ for _, field := range fields {
+ n := []rune(field.Name)
+ if len(n) > maximumProfileFieldLength {
+ field.Name = string(n[:maximumProfileFieldLength])
+ }
+
+ v := []rune(field.Value)
+ if len(v) > maximumProfileFieldLength {
+ field.Value = string(v[:maximumProfileFieldLength])
+ }
}
- return string(*f)
+ return nil
}
diff --git a/internal/validate/formvalidation_test.go b/internal/validate/formvalidation_test.go
index fa59977b9..9cfa07700 100644
--- a/internal/validate/formvalidation_test.go
+++ b/internal/validate/formvalidation_test.go
@@ -24,7 +24,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/validate"
)
@@ -268,37 +268,34 @@ func (suite *ValidationTestSuite) TestValidateReason() {
}
}
-func (suite *ValidationTestSuite) TestValidateProfileFieldsCount() {
- noFields := []model.UpdateField{}
- fewFields := []model.UpdateField{{}, {}}
- tooManyFields := []model.UpdateField{{}, {}, {}, {}, {}}
- err := validate.ProfileFieldsCount(tooManyFields)
- if assert.Error(suite.T(), err) {
- assert.Equal(suite.T(), errors.New("cannot have more than 4 profile fields"), err)
- }
-
- err = validate.ProfileFieldsCount(noFields)
- assert.NoError(suite.T(), err)
-
- err = validate.ProfileFieldsCount(fewFields)
- assert.NoError(suite.T(), err)
-}
-
func (suite *ValidationTestSuite) TestValidateProfileField() {
- shortProfileField := "pronouns"
- tooLongProfileField := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui eleifend massa nunc."
- trimmedProfileField := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui "
-
- validated := validate.ProfileField(&shortProfileField)
- assert.Equal(suite.T(), shortProfileField, validated)
-
- validated = validate.ProfileField(&tooLongProfileField)
- assert.Len(suite.T(), validated, 255)
- assert.Equal(suite.T(), trimmedProfileField, validated)
+ var (
+ shortProfileField = "pronouns"
+ tooLongProfileField = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui eleifend massa nunc."
+ trimmedProfileField = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer eu bibendum elit. Sed ac interdum nisi. Vestibulum vulputate eros quis euismod imperdiet. Nulla sit amet dui sit amet lorem consectetur iaculis. Mauris eget lacinia metus. Curabitur nec dui "
+ err error
+ )
+
+ okFields := []*gtsmodel.Field{
+ {
+ Name: "example",
+ Value: shortProfileField,
+ },
+ }
+ err = validate.ProfileFields(okFields)
+ suite.NoError(err)
+ suite.Equal(shortProfileField, okFields[0].Value)
- validated = validate.ProfileField(&trimmedProfileField)
- assert.Len(suite.T(), validated, 255)
- assert.Equal(suite.T(), trimmedProfileField, validated)
+ dodgyFields := []*gtsmodel.Field{
+ {
+ Name: "example",
+ Value: tooLongProfileField,
+ },
+ }
+ err = validate.ProfileFields(dodgyFields)
+ suite.NoError(err)
+ suite.Equal(trimmedProfileField, dodgyFields[0].Value)
+ suite.Len(dodgyFields[0].Value, 255)
}
func TestValidationTestSuite(t *testing.T) {