[chore] internal/ap: add pollable AS types, code reformatting, general niceties (#2248)

This commit is contained in:
kim 2023-10-03 14:59:30 +01:00 committed by GitHub
parent a1ab2c255a
commit 297b6eeaaa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 559 additions and 224 deletions

View file

@ -78,3 +78,49 @@
// and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag // and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
TagHashtag = "Hashtag" TagHashtag = "Hashtag"
) )
// isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity).
func isActivity(typeName string) bool {
switch typeName {
case ActivityAccept,
ActivityTentativeAccept,
ActivityAdd,
ActivityCreate,
ActivityDelete,
ActivityFollow,
ActivityIgnore,
ActivityJoin,
ActivityLeave,
ActivityLike,
ActivityOffer,
ActivityInvite,
ActivityReject,
ActivityTentativeReject,
ActivityRemove,
ActivityUndo,
ActivityUpdate,
ActivityView,
ActivityListen,
ActivityRead,
ActivityMove,
ActivityAnnounce,
ActivityBlock,
ActivityFlag,
ActivityDislike:
return true
default:
return false
}
}
// isIntransitiveActivity returns whether AS type name is of an IntransitiveActivity.
func isIntransitiveActivity(typeName string) bool {
switch typeName {
case ActivityArrive,
ActivityTravel,
ActivityQuestion:
return true
default:
return false
}
}

View file

@ -28,12 +28,53 @@
"time" "time"
"github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/text"
"github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/internal/util"
) )
// ExtractObject will extract an object vocab.Type from given implementing interface.
func ExtractObject(with WithObject) vocab.Type {
// Extract the attached object (if any).
obj := with.GetActivityStreamsObject()
if obj == nil {
return nil
}
// Only support single
// objects (for now...)
if obj.Len() != 1 {
return nil
}
// Extract object vocab.Type.
return obj.At(0).GetType()
}
// ExtractActivityData will extract the usable data type (e.g. Note, Question, etc) and corresponding JSON, from activity.
func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) (vocab.Type, map[string]any, bool) {
switch typeName := activity.GetTypeName(); {
// Activity (has "object").
case isActivity(typeName):
objType := ExtractObject(activity)
if objType == nil {
return nil, nil, false
}
objJSON, _ := rawJSON["object"].(map[string]any)
return objType, objJSON, true
// IntransitiveAcitivity (no "object").
case isIntransitiveActivity(typeName):
return activity, rawJSON, false
// Unknown.
default:
return nil, nil, false
}
}
// ExtractPreferredUsername returns a string representation of // ExtractPreferredUsername returns a string representation of
// an interface's preferredUsername property. Will return an // an interface's preferredUsername property. Will return an
// error if preferredUsername is nil, not a string, or empty. // error if preferredUsername is nil, not a string, or empty.
@ -497,6 +538,38 @@ func ExtractContent(i WithContent) string {
return "" return ""
} }
// ExtractAttachments attempts to extract barebones MediaAttachment objects from given AS interface type.
func ExtractAttachments(i WithAttachment) ([]*gtsmodel.MediaAttachment, error) {
attachmentProp := i.GetActivityStreamsAttachment()
if attachmentProp == nil {
return nil, nil
}
var errs gtserror.MultiError
attachments := make([]*gtsmodel.MediaAttachment, 0, attachmentProp.Len())
for iter := attachmentProp.Begin(); iter != attachmentProp.End(); iter = iter.Next() {
t := iter.GetType()
if t == nil {
errs.Appendf("nil attachment type")
continue
}
attachmentable, ok := t.(Attachmentable)
if !ok {
errs.Appendf("incorrect attachment type: %T", t)
continue
}
attachment, err := ExtractAttachment(attachmentable)
if err != nil {
errs.Appendf("error extracting attachment: %w", err)
continue
}
attachments = append(attachments, attachment)
}
return attachments, errs.Combine()
}
// ExtractAttachment extracts a minimal gtsmodel.Attachment // ExtractAttachment extracts a minimal gtsmodel.Attachment
// (just remote URL, description, and blurhash) from the given // (just remote URL, description, and blurhash) from the given
// Attachmentable interface, or an error if no remote URL is set. // Attachmentable interface, or an error if no remote URL is set.
@ -913,6 +986,52 @@ func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL {
return nil return nil
} }
// IterateOneOf will attempt to extract oneOf property from given interface, and passes each iterated item to function.
func IterateOneOf(withOneOf WithOneOf, foreach func(vocab.ActivityStreamsOneOfPropertyIterator)) {
if foreach == nil {
// nil check outside loop.
panic("nil function")
}
// Extract the one-of property from interface.
oneOfProp := withOneOf.GetActivityStreamsOneOf()
if oneOfProp == nil {
return
}
// Get start and end of iter.
start := oneOfProp.Begin()
end := oneOfProp.End()
// Pass iterated oneOf entries to given function.
for iter := start; iter != end; iter = iter.Next() {
foreach(iter)
}
}
// IterateAnyOf will attempt to extract anyOf property from given interface, and passes each iterated item to function.
func IterateAnyOf(withAnyOf WithAnyOf, foreach func(vocab.ActivityStreamsAnyOfPropertyIterator)) {
if foreach == nil {
// nil check outside loop.
panic("nil function")
}
// Extract the any-of property from interface.
anyOfProp := withAnyOf.GetActivityStreamsAnyOf()
if anyOfProp == nil {
return
}
// Get start and end of iter.
start := anyOfProp.Begin()
end := anyOfProp.End()
// Pass iterated anyOf entries to given function.
for iter := start; iter != end; iter = iter.Next() {
foreach(iter)
}
}
// isPublic checks if at least one entry in the given // isPublic checks if at least one entry in the given
// uris slice equals the activitystreams public uri. // uris slice equals the activitystreams public uri.
func isPublic(uris []*url.URL) bool { func isPublic(uris []*url.URL) bool {

View file

@ -23,11 +23,76 @@
"github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/activity/streams/vocab"
) )
// IsAccountable returns whether AS vocab type name is acceptable as Accountable.
func IsAccountable(typeName string) bool {
switch typeName {
case ActorPerson,
ActorApplication,
ActorOrganization,
ActorService,
ActorGroup:
return true
default:
return false
}
}
// ToAccountable safely tries to cast vocab.Type as Accountable, also checking for expected AS type names.
func ToAccountable(t vocab.Type) (Accountable, bool) {
accountable, ok := t.(Accountable)
if !ok || !IsAccountable(t.GetTypeName()) {
return nil, false
}
return accountable, true
}
// IsStatusable returns whether AS vocab type name is acceptable as Statusable.
func IsStatusable(typeName string) bool {
switch typeName {
case ObjectArticle,
ObjectDocument,
ObjectImage,
ObjectVideo,
ObjectNote,
ObjectPage,
ObjectEvent,
ObjectPlace,
ObjectProfile,
ActivityQuestion:
return true
default:
return false
}
}
// ToStatusable safely tries to cast vocab.Type as Statusable, also checking for expected AS type names.
func ToStatusable(t vocab.Type) (Statusable, bool) {
statusable, ok := t.(Statusable)
if !ok || !IsStatusable(t.GetTypeName()) {
return nil, false
}
return statusable, true
}
// IsPollable returns whether AS vocab type name is acceptable as Pollable.
func IsPollable(typeName string) bool {
return typeName == ActivityQuestion
}
// ToPollable safely tries to cast vocab.Type as Pollable, also checking for expected AS type names.
func ToPollable(t vocab.Type) (Pollable, bool) {
pollable, ok := t.(Pollable)
if !ok || !IsPollable(t.GetTypeName()) {
return nil, false
}
return pollable, true
}
// Accountable represents the minimum activitypub interface for representing an 'account'. // Accountable represents the minimum activitypub interface for representing an 'account'.
// This interface is fulfilled by: Person, Application, Organization, Service, and Group // (see: IsAccountable() for types implementing this, though you MUST make sure to check
// the typeName as this bare interface may be implementable by non-Accountable types).
type Accountable interface { type Accountable interface {
WithJSONLDId vocab.Type
WithTypeName
WithPreferredUsername WithPreferredUsername
WithIcon WithIcon
@ -35,7 +100,6 @@ type Accountable interface {
WithImage WithImage
WithSummary WithSummary
WithAttachment WithAttachment
WithSetSummary
WithDiscoverable WithDiscoverable
WithURL WithURL
WithPublicKey WithPublicKey
@ -50,15 +114,13 @@ type Accountable interface {
} }
// Statusable represents the minimum activitypub interface for representing a 'status'. // Statusable represents the minimum activitypub interface for representing a 'status'.
// This interface is fulfilled by: Article, Document, Image, Video, Note, Page, Event, Place, Mention, Profile // (see: IsStatusable() for types implementing this, though you MUST make sure to check
// the typeName as this bare interface may be implementable by non-Statusable types).
type Statusable interface { type Statusable interface {
WithJSONLDId vocab.Type
WithTypeName
WithSummary WithSummary
WithSetSummary
WithName WithName
WithSetName
WithInReplyTo WithInReplyTo
WithPublished WithPublished
WithURL WithURL
@ -68,20 +130,40 @@ type Statusable interface {
WithSensitive WithSensitive
WithConversation WithConversation
WithContent WithContent
WithSetContent
WithAttachment WithAttachment
WithTag WithTag
WithReplies WithReplies
} }
// Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. // Pollable represents the minimum activitypub interface for representing a 'poll' (it's a subset of a status).
// (see: IsPollable() for types implementing this, though you MUST make sure to check
// the typeName as this bare interface may be implementable by non-Pollable types).
type Pollable interface {
WithOneOf
WithAnyOf
WithEndTime
WithClosed
WithVotersCount
// base-interface
Statusable
}
// PollOptionable represents the minimum activitypub interface for representing a poll 'option'.
// (see: IsPollOptionable() for types implementing this).
type PollOptionable interface {
WithTypeName
WithName
WithReplies
}
// Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. (see: IsAttachmentable).
// This interface is fulfilled by: Audio, Document, Image, Video // This interface is fulfilled by: Audio, Document, Image, Video
type Attachmentable interface { type Attachmentable interface {
WithTypeName WithTypeName
WithMediaType WithMediaType
WithURL WithURL
WithName WithName
WithSetName
WithBlurhash WithBlurhash
} }
@ -160,8 +242,7 @@ type ReplyToable interface {
// CollectionPageIterator represents the minimum interface for interacting with a wrapped // CollectionPageIterator represents the minimum interface for interacting with a wrapped
// CollectionPage or OrderedCollectionPage in order to access both next / prev pages and items. // CollectionPage or OrderedCollectionPage in order to access both next / prev pages and items.
type CollectionPageIterator interface { type CollectionPageIterator interface {
WithJSONLDId vocab.Type
WithTypeName
NextPage() WithIRI NextPage() WithIRI
PrevPage() WithIRI PrevPage() WithIRI
@ -189,12 +270,14 @@ type Flaggable interface {
// WithJSONLDId represents an activity with JSONLDIdProperty. // WithJSONLDId represents an activity with JSONLDIdProperty.
type WithJSONLDId interface { type WithJSONLDId interface {
GetJSONLDId() vocab.JSONLDIdProperty GetJSONLDId() vocab.JSONLDIdProperty
SetJSONLDId(vocab.JSONLDIdProperty)
} }
// WithIRI represents an object (possibly) representable as an IRI. // WithIRI represents an object (possibly) representable as an IRI.
type WithIRI interface { type WithIRI interface {
GetIRI() *url.URL GetIRI() *url.URL
IsIRI() bool IsIRI() bool
SetIRI(*url.URL)
} }
// WithType ... // WithType ...
@ -210,20 +293,18 @@ type WithTypeName interface {
// WithPreferredUsername represents an activity with ActivityStreamsPreferredUsernameProperty // WithPreferredUsername represents an activity with ActivityStreamsPreferredUsernameProperty
type WithPreferredUsername interface { type WithPreferredUsername interface {
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
SetActivityStreamsPreferredUsername(vocab.ActivityStreamsPreferredUsernameProperty)
} }
// WithIcon represents an activity with ActivityStreamsIconProperty // WithIcon represents an activity with ActivityStreamsIconProperty
type WithIcon interface { type WithIcon interface {
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
SetActivityStreamsIcon(vocab.ActivityStreamsIconProperty)
} }
// WithName represents an activity with ActivityStreamsNameProperty // WithName represents an activity with ActivityStreamsNameProperty
type WithName interface { type WithName interface {
GetActivityStreamsName() vocab.ActivityStreamsNameProperty GetActivityStreamsName() vocab.ActivityStreamsNameProperty
}
// WithSetName represents an activity with a settable ActivityStreamsNameProperty
type WithSetName interface {
SetActivityStreamsName(vocab.ActivityStreamsNameProperty) SetActivityStreamsName(vocab.ActivityStreamsNameProperty)
} }
@ -235,81 +316,91 @@ type WithImage interface {
// WithSummary represents an activity with ActivityStreamsSummaryProperty // WithSummary represents an activity with ActivityStreamsSummaryProperty
type WithSummary interface { type WithSummary interface {
GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty
}
// WithSetSummary represents an activity that can have summary set on it.
type WithSetSummary interface {
SetActivityStreamsSummary(vocab.ActivityStreamsSummaryProperty) SetActivityStreamsSummary(vocab.ActivityStreamsSummaryProperty)
} }
// WithDiscoverable represents an activity with TootDiscoverableProperty // WithDiscoverable represents an activity with TootDiscoverableProperty
type WithDiscoverable interface { type WithDiscoverable interface {
GetTootDiscoverable() vocab.TootDiscoverableProperty GetTootDiscoverable() vocab.TootDiscoverableProperty
SetTootDiscoverable(vocab.TootDiscoverableProperty)
} }
// WithURL represents an activity with ActivityStreamsUrlProperty // WithURL represents an activity with ActivityStreamsUrlProperty
type WithURL interface { type WithURL interface {
GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty
SetActivityStreamsUrl(vocab.ActivityStreamsUrlProperty)
} }
// WithPublicKey represents an activity with W3IDSecurityV1PublicKeyProperty // WithPublicKey represents an activity with W3IDSecurityV1PublicKeyProperty
type WithPublicKey interface { type WithPublicKey interface {
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
SetW3IDSecurityV1PublicKey(vocab.W3IDSecurityV1PublicKeyProperty)
} }
// WithInbox represents an activity with ActivityStreamsInboxProperty // WithInbox represents an activity with ActivityStreamsInboxProperty
type WithInbox interface { type WithInbox interface {
GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
SetActivityStreamsInbox(vocab.ActivityStreamsInboxProperty)
} }
// WithOutbox represents an activity with ActivityStreamsOutboxProperty // WithOutbox represents an activity with ActivityStreamsOutboxProperty
type WithOutbox interface { type WithOutbox interface {
GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty
SetActivityStreamsOutbox(vocab.ActivityStreamsOutboxProperty)
} }
// WithFollowing represents an activity with ActivityStreamsFollowingProperty // WithFollowing represents an activity with ActivityStreamsFollowingProperty
type WithFollowing interface { type WithFollowing interface {
GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty
SetActivityStreamsFollowing(vocab.ActivityStreamsFollowingProperty)
} }
// WithFollowers represents an activity with ActivityStreamsFollowersProperty // WithFollowers represents an activity with ActivityStreamsFollowersProperty
type WithFollowers interface { type WithFollowers interface {
GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty
SetActivityStreamsFollowers(vocab.ActivityStreamsFollowersProperty)
} }
// WithFeatured represents an activity with TootFeaturedProperty // WithFeatured represents an activity with TootFeaturedProperty
type WithFeatured interface { type WithFeatured interface {
GetTootFeatured() vocab.TootFeaturedProperty GetTootFeatured() vocab.TootFeaturedProperty
SetTootFeatured(vocab.TootFeaturedProperty)
} }
// WithAttributedTo represents an activity with ActivityStreamsAttributedToProperty // WithAttributedTo represents an activity with ActivityStreamsAttributedToProperty
type WithAttributedTo interface { type WithAttributedTo interface {
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
SetActivityStreamsAttributedTo(vocab.ActivityStreamsAttributedToProperty)
} }
// WithAttachment represents an activity with ActivityStreamsAttachmentProperty // WithAttachment represents an activity with ActivityStreamsAttachmentProperty
type WithAttachment interface { type WithAttachment interface {
GetActivityStreamsAttachment() vocab.ActivityStreamsAttachmentProperty GetActivityStreamsAttachment() vocab.ActivityStreamsAttachmentProperty
SetActivityStreamsAttachment(vocab.ActivityStreamsAttachmentProperty)
} }
// WithTo represents an activity with ActivityStreamsToProperty // WithTo represents an activity with ActivityStreamsToProperty
type WithTo interface { type WithTo interface {
GetActivityStreamsTo() vocab.ActivityStreamsToProperty GetActivityStreamsTo() vocab.ActivityStreamsToProperty
SetActivityStreamsTo(vocab.ActivityStreamsToProperty)
} }
// WithInReplyTo represents an activity with ActivityStreamsInReplyToProperty // WithInReplyTo represents an activity with ActivityStreamsInReplyToProperty
type WithInReplyTo interface { type WithInReplyTo interface {
GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty
SetActivityStreamsInReplyTo(vocab.ActivityStreamsInReplyToProperty)
} }
// WithCC represents an activity with ActivityStreamsCcProperty // WithCC represents an activity with ActivityStreamsCcProperty
type WithCC interface { type WithCC interface {
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
SetActivityStreamsCc(vocab.ActivityStreamsCcProperty)
} }
// WithSensitive represents an activity with ActivityStreamsSensitiveProperty // WithSensitive represents an activity with ActivityStreamsSensitiveProperty
type WithSensitive interface { type WithSensitive interface {
GetActivityStreamsSensitive() vocab.ActivityStreamsSensitiveProperty GetActivityStreamsSensitive() vocab.ActivityStreamsSensitiveProperty
SetActivityStreamsSensitive(vocab.ActivityStreamsSensitiveProperty)
} }
// WithConversation ... // WithConversation ...
@ -319,36 +410,37 @@ type WithConversation interface {
// WithContent represents an activity with ActivityStreamsContentProperty // WithContent represents an activity with ActivityStreamsContentProperty
type WithContent interface { type WithContent interface {
GetActivityStreamsContent() vocab.ActivityStreamsContentProperty GetActivityStreamsContent() vocab.ActivityStreamsContentProperty
}
// WithSetContent represents an activity that can have content set on it.
type WithSetContent interface {
SetActivityStreamsContent(vocab.ActivityStreamsContentProperty) SetActivityStreamsContent(vocab.ActivityStreamsContentProperty)
} }
// WithPublished represents an activity with ActivityStreamsPublishedProperty // WithPublished represents an activity with ActivityStreamsPublishedProperty
type WithPublished interface { type WithPublished interface {
GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty
SetActivityStreamsPublished(vocab.ActivityStreamsPublishedProperty)
} }
// WithTag represents an activity with ActivityStreamsTagProperty // WithTag represents an activity with ActivityStreamsTagProperty
type WithTag interface { type WithTag interface {
GetActivityStreamsTag() vocab.ActivityStreamsTagProperty GetActivityStreamsTag() vocab.ActivityStreamsTagProperty
SetActivityStreamsTag(vocab.ActivityStreamsTagProperty)
} }
// WithReplies represents an activity with ActivityStreamsRepliesProperty // WithReplies represents an activity with ActivityStreamsRepliesProperty
type WithReplies interface { type WithReplies interface {
GetActivityStreamsReplies() vocab.ActivityStreamsRepliesProperty GetActivityStreamsReplies() vocab.ActivityStreamsRepliesProperty
SetActivityStreamsReplies(vocab.ActivityStreamsRepliesProperty)
} }
// WithMediaType represents an activity with ActivityStreamsMediaTypeProperty // WithMediaType represents an activity with ActivityStreamsMediaTypeProperty
type WithMediaType interface { type WithMediaType interface {
GetActivityStreamsMediaType() vocab.ActivityStreamsMediaTypeProperty GetActivityStreamsMediaType() vocab.ActivityStreamsMediaTypeProperty
SetActivityStreamsMediaType(vocab.ActivityStreamsMediaTypeProperty)
} }
// WithBlurhash represents an activity with TootBlurhashProperty // WithBlurhash represents an activity with TootBlurhashProperty
type WithBlurhash interface { type WithBlurhash interface {
GetTootBlurhash() vocab.TootBlurhashProperty GetTootBlurhash() vocab.TootBlurhashProperty
SetTootBlurhash(vocab.TootBlurhashProperty)
} }
// type withFocalPoint interface { // type withFocalPoint interface {
@ -358,44 +450,83 @@ type WithBlurhash interface {
// WithHref represents an activity with ActivityStreamsHrefProperty // WithHref represents an activity with ActivityStreamsHrefProperty
type WithHref interface { type WithHref interface {
GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty
SetActivityStreamsHref(vocab.ActivityStreamsHrefProperty)
} }
// WithUpdated represents an activity with ActivityStreamsUpdatedProperty // WithUpdated represents an activity with ActivityStreamsUpdatedProperty
type WithUpdated interface { type WithUpdated interface {
GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty
SetActivityStreamsUpdated(vocab.ActivityStreamsUpdatedProperty)
} }
// WithActor represents an activity with ActivityStreamsActorProperty // WithActor represents an activity with ActivityStreamsActorProperty
type WithActor interface { type WithActor interface {
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
SetActivityStreamsActor(vocab.ActivityStreamsActorProperty)
} }
// WithObject represents an activity with ActivityStreamsObjectProperty // WithObject represents an activity with ActivityStreamsObjectProperty
type WithObject interface { type WithObject interface {
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty)
} }
// WithNext represents an activity with ActivityStreamsNextProperty // WithNext represents an activity with ActivityStreamsNextProperty
type WithNext interface { type WithNext interface {
GetActivityStreamsNext() vocab.ActivityStreamsNextProperty GetActivityStreamsNext() vocab.ActivityStreamsNextProperty
SetActivityStreamsNext(vocab.ActivityStreamsNextProperty)
} }
// WithPartOf represents an activity with ActivityStreamsPartOfProperty // WithPartOf represents an activity with ActivityStreamsPartOfProperty
type WithPartOf interface { type WithPartOf interface {
GetActivityStreamsPartOf() vocab.ActivityStreamsPartOfProperty GetActivityStreamsPartOf() vocab.ActivityStreamsPartOfProperty
SetActivityStreamsPartOf(vocab.ActivityStreamsPartOfProperty)
} }
// WithItems represents an activity with ActivityStreamsItemsProperty // WithItems represents an activity with ActivityStreamsItemsProperty
type WithItems interface { type WithItems interface {
GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty
SetActivityStreamsItems(vocab.ActivityStreamsItemsProperty)
} }
// WithManuallyApprovesFollowers represents a Person or profile with the ManuallyApprovesFollowers property. // WithManuallyApprovesFollowers represents a Person or profile with the ManuallyApprovesFollowers property.
type WithManuallyApprovesFollowers interface { type WithManuallyApprovesFollowers interface {
GetActivityStreamsManuallyApprovesFollowers() vocab.ActivityStreamsManuallyApprovesFollowersProperty GetActivityStreamsManuallyApprovesFollowers() vocab.ActivityStreamsManuallyApprovesFollowersProperty
SetActivityStreamsManuallyApprovesFollowers(vocab.ActivityStreamsManuallyApprovesFollowersProperty)
} }
// WithEndpoints represents a Person or profile with the endpoints property // WithEndpoints represents a Person or profile with the endpoints property
type WithEndpoints interface { type WithEndpoints interface {
GetActivityStreamsEndpoints() vocab.ActivityStreamsEndpointsProperty GetActivityStreamsEndpoints() vocab.ActivityStreamsEndpointsProperty
SetActivityStreamsEndpoints(vocab.ActivityStreamsEndpointsProperty)
}
// WithOneOf represents an activity with the oneOf property.
type WithOneOf interface {
GetActivityStreamsOneOf() vocab.ActivityStreamsOneOfProperty
SetActivityStreamsOneOf(vocab.ActivityStreamsOneOfProperty)
}
// WithOneOf represents an activity with the oneOf property.
type WithAnyOf interface {
GetActivityStreamsAnyOf() vocab.ActivityStreamsAnyOfProperty
SetActivityStreamsAnyOf(vocab.ActivityStreamsAnyOfProperty)
}
// WithEndTime represents an activity with the endTime property.
type WithEndTime interface {
GetActivityStreamsEndTime() vocab.ActivityStreamsEndTimeProperty
SetActivityStreamsEndTime(vocab.ActivityStreamsEndTimeProperty)
}
// WithClosed represents an activity with the closed property.
type WithClosed interface {
GetActivityStreamsClosed() vocab.ActivityStreamsClosedProperty
SetActivityStreamsClosed(vocab.ActivityStreamsClosedProperty)
}
// WithVotersCount represents an activity with the votersCount property.
type WithVotersCount interface {
GetTootVotersCount() vocab.TootVotersCountProperty
SetTootVotersCount(vocab.TootVotersCountProperty)
} }

View file

@ -37,92 +37,62 @@
// The rawActivity map should the freshly deserialized json representation of the 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 or Update with a Statusable or Accountable as its Object. // 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{}) { func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
if typeName := activity.GetTypeName(); typeName != ActivityCreate && typeName != ActivityUpdate { // From the activity extract the data vocab.Type + its "raw" JSON.
// Only interested in Create or Update right now. dataType, rawData, ok := ExtractActivityData(activity, rawJSON)
return
}
withObject, ok := activity.(WithObject)
if !ok { if !ok {
// Create was not a WithObject.
return return
} }
createObject := withObject.GetActivityStreamsObject() switch dataType.GetTypeName() {
if createObject == nil { // "Pollable" types.
// No object set. case ActivityQuestion:
return pollable, ok := dataType.(Pollable)
}
if createObject.Len() != 1 {
// Not interested in Object arrays.
return
}
// We now know length is 1 so get the first
// 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.
return
}
t := i.GetType()
if t == nil {
// This is also awkward.
return
}
switch t.GetTypeName() {
case ObjectArticle, ObjectDocument, ObjectImage, ObjectVideo, ObjectNote, ObjectPage, ObjectEvent, ObjectPlace, ObjectProfile:
statusable, ok := t.(Statusable)
if !ok { if !ok {
// Object is not Statusable;
// we're not interested.
return return
} }
rawObject, ok := rawJSON["object"] // Normalize the Pollable specific properties.
if !ok { NormalizeIncomingPollOptions(pollable, rawData)
// No object in raw map.
return
}
rawStatusableJSON, ok := rawObject.(map[string]interface{}) // Fallthrough to handle
// the rest as Statusable.
fallthrough
// "Statusable" types.
case ObjectArticle,
ObjectDocument,
ObjectImage,
ObjectVideo,
ObjectNote,
ObjectPage,
ObjectEvent,
ObjectPlace,
ObjectProfile:
statusable, ok := dataType.(Statusable)
if !ok { if !ok {
// Object wasn't a json object.
return return
} }
// Normalize everything we can on the statusable. // Normalize everything we can on the statusable.
NormalizeIncomingContent(statusable, rawStatusableJSON) NormalizeIncomingContent(statusable, rawData)
NormalizeIncomingAttachments(statusable, rawStatusableJSON) NormalizeIncomingAttachments(statusable, rawData)
NormalizeIncomingSummary(statusable, rawStatusableJSON) NormalizeIncomingSummary(statusable, rawData)
NormalizeIncomingName(statusable, rawStatusableJSON) NormalizeIncomingName(statusable, rawData)
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"] // "Accountable" types.
case ActorApplication,
ActorGroup,
ActorOrganization,
ActorPerson,
ActorService:
accountable, ok := dataType.(Accountable)
if !ok { if !ok {
// No object in raw map.
return
}
rawAccountableJSON, ok := rawObject.(map[string]interface{})
if !ok {
// Object wasn't a json object.
return return
} }
// Normalize everything we can on the accountable. // Normalize everything we can on the accountable.
NormalizeIncomingSummary(accountable, rawAccountableJSON) NormalizeIncomingSummary(accountable, rawData)
} }
} }
@ -132,7 +102,7 @@ func NormalizeIncomingActivityObject(activity pub.Activity, rawJSON map[string]i
// //
// noop if there was no content in the json object map or the // noop if there was no content in the json object map or the
// content was not a plain string. // content was not a plain string.
func NormalizeIncomingContent(item WithSetContent, rawJSON map[string]interface{}) { func NormalizeIncomingContent(item WithContent, rawJSON map[string]interface{}) {
rawContent, ok := rawJSON["content"] rawContent, ok := rawJSON["content"]
if !ok { if !ok {
// No content in rawJSON. // No content in rawJSON.
@ -228,7 +198,7 @@ func NormalizeIncomingAttachments(item WithAttachment, rawJSON map[string]interf
// //
// noop if there was no summary in the json object map or the // noop if there was no summary in the json object map or the
// summary was not a plain string. // summary was not a plain string.
func NormalizeIncomingSummary(item WithSetSummary, rawJSON map[string]interface{}) { func NormalizeIncomingSummary(item WithSummary, rawJSON map[string]interface{}) {
rawSummary, ok := rawJSON["summary"] rawSummary, ok := rawJSON["summary"]
if !ok { if !ok {
// No summary in rawJSON. // No summary in rawJSON.
@ -258,7 +228,7 @@ func NormalizeIncomingSummary(item WithSetSummary, rawJSON map[string]interface{
// //
// noop if there was no name in the json object map or the // noop if there was no name in the json object map or the
// name was not a plain string. // name was not a plain string.
func NormalizeIncomingName(item WithSetName, rawJSON map[string]interface{}) { func NormalizeIncomingName(item WithName, rawJSON map[string]interface{}) {
rawName, ok := rawJSON["name"] rawName, ok := rawJSON["name"]
if !ok { if !ok {
// No name in rawJSON. // No name in rawJSON.
@ -284,3 +254,60 @@ func NormalizeIncomingName(item WithSetName, rawJSON map[string]interface{}) {
nameProp.AppendXMLSchemaString(name) nameProp.AppendXMLSchemaString(name)
item.SetActivityStreamsName(nameProp) item.SetActivityStreamsName(nameProp)
} }
// NormalizeIncomingOneOf normalizes all oneOf (if any) of the given
// item, replacing the 'name' field of each oneOf with the raw 'name'
// value from the raw json object map, and doing sanitization
// on the result.
//
// noop if there are no oneOf; noop if oneOf is not expected format.
func NormalizeIncomingPollOptions(item WithOneOf, rawJSON map[string]interface{}) {
var oneOf []interface{}
// Get the raw one-of JSON data.
rawOneOf, ok := rawJSON["oneOf"]
if !ok {
return
}
// Convert to slice if not already, so we can iterate.
if oneOf, ok = rawOneOf.([]interface{}); !ok {
oneOf = []interface{}{rawOneOf}
}
// Extract the one-of property from interface.
oneOfProp := item.GetActivityStreamsOneOf()
if oneOfProp == nil {
return
}
// Check we have useable one-of JSON-vs-unmarshaled data.
if l := oneOfProp.Len(); l == 0 || l != len(oneOf) {
return
}
// Get start and end of iter.
start := oneOfProp.Begin()
end := oneOfProp.End()
// Iterate a counter, from start through to end iter item.
for i, iter := 0, start; iter != end; i, iter = i+1, iter.Next() {
// Get item type.
t := iter.GetType()
// Check fulfills Choiceable type
// (this accounts for nil input type).
choiceable, ok := t.(PollOptionable)
if !ok {
continue
}
// Get the corresponding raw one-of data.
rawChoice, ok := oneOf[i].(map[string]interface{})
if !ok {
continue
}
NormalizeIncomingName(choiceable, rawChoice)
}
}

View file

@ -191,7 +191,7 @@ func (suite *NormalizeTestSuite) TestNormalizeActivityObject() {
note, note,
) )
ap.NormalizeIncomingActivityObject(create, map[string]interface{}{"object": rawNote}) ap.NormalizeIncomingActivity(create, map[string]interface{}{"object": rawNote})
suite.Equal(`UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`, ap.ExtractContent(note)) suite.Equal(`UPDATE: As of this morning there are now more than 7 million Mastodon users, most from the <a class="hashtag" href="https://example.org/tag/twittermigration" rel="tag ugc nofollow noreferrer noopener" target="_blank">#TwitterMigration</a>.<br><br>In fact, 100,000 new accounts have been created since last night.<br><br>Since last night's spike 8,000-12,000 new accounts are being created every hour.<br><br>Yesterday, I estimated that Mastodon would have 8 million users by the end of the week. That might happen a lot sooner if this trend continues.`, ap.ExtractContent(note))
} }

View file

@ -20,62 +20,134 @@
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"net/http"
"sync"
"github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtserror"
) )
// mapPool is a memory pool of maps for JSON decoding.
var mapPool = sync.Pool{
New: func() any {
return make(map[string]any)
},
}
// getMap acquires a map from memory pool.
func getMap() map[string]any {
m := mapPool.Get().(map[string]any) //nolint
return m
}
// putMap clears and places map back in pool.
func putMap(m map[string]any) {
if len(m) > int(^uint8(0)) {
// don't pool overly
// large maps.
return
}
for k := range m {
delete(m, k)
}
mapPool.Put(m)
}
// ResolveActivity is a util function for pulling a pub.Activity type out of an incoming request body.
func ResolveIncomingActivity(r *http.Request) (pub.Activity, gtserror.WithCode) {
// Get "raw" map
// destination.
raw := getMap()
// Tidy up when done.
defer r.Body.Close()
// Decode the JSON body stream into "raw" map.
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
err := gtserror.Newf("error decoding json: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Resolve "raw" JSON to vocab.Type.
t, err := streams.ToType(r.Context(), raw)
if err != nil {
if !streams.IsUnmatchedErr(err) {
err := gtserror.Newf("error matching json to type: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Respond with bad request; we just couldn't
// match the type to one that we know about.
const text = "body json not resolvable as ActivityStreams type"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
// Ensure this is an Activity type.
activity, ok := t.(pub.Activity)
if !ok {
text := fmt.Sprintf("cannot resolve vocab type %T as pub.Activity", t)
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
if activity.GetJSONLDId() == nil {
const text = "missing ActivityStreams id property"
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
}
// Normalize any Statusable, Accountable, Pollable fields found.
// (see: https://github.com/superseriousbusiness/gotosocial/issues/1661)
NormalizeIncomingActivity(activity, raw)
// Release.
putMap(raw)
return activity, nil
}
// ResolveStatusable tries to resolve the given bytes into an ActivityPub Statusable representation. // ResolveStatusable tries to resolve the given bytes into an ActivityPub Statusable representation.
// It will then perform normalization on the Statusable. // It will then perform normalization on the Statusable.
// //
// Works for: Article, Document, Image, Video, Note, Page, Event, Place, Profile // Works for: Article, Document, Image, Video, Note, Page, Event, Place, Profile, Question.
func ResolveStatusable(ctx context.Context, b []byte) (Statusable, error) { func ResolveStatusable(ctx context.Context, b []byte) (Statusable, error) {
rawStatusable := make(map[string]interface{}) // Get "raw" map
if err := json.Unmarshal(b, &rawStatusable); err != nil { // destination.
raw := getMap()
// Unmarshal the raw JSON data in a "raw" JSON map.
if err := json.Unmarshal(b, &raw); err != nil {
return nil, gtserror.Newf("error unmarshalling bytes into json: %w", err) return nil, gtserror.Newf("error unmarshalling bytes into json: %w", err)
} }
t, err := streams.ToType(ctx, rawStatusable) // Resolve an ActivityStreams type from JSON.
t, err := streams.ToType(ctx, raw)
if err != nil { if err != nil {
return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err) return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err)
} }
var ( // Attempt to cast as Statusable.
statusable Statusable statusable, ok := ToStatusable(t)
ok bool
)
switch t.GetTypeName() {
case ObjectArticle:
statusable, ok = t.(vocab.ActivityStreamsArticle)
case ObjectDocument:
statusable, ok = t.(vocab.ActivityStreamsDocument)
case ObjectImage:
statusable, ok = t.(vocab.ActivityStreamsImage)
case ObjectVideo:
statusable, ok = t.(vocab.ActivityStreamsVideo)
case ObjectNote:
statusable, ok = t.(vocab.ActivityStreamsNote)
case ObjectPage:
statusable, ok = t.(vocab.ActivityStreamsPage)
case ObjectEvent:
statusable, ok = t.(vocab.ActivityStreamsEvent)
case ObjectPlace:
statusable, ok = t.(vocab.ActivityStreamsPlace)
case ObjectProfile:
statusable, ok = t.(vocab.ActivityStreamsProfile)
}
if !ok { if !ok {
err = gtserror.Newf("could not resolve %T to Statusable", t) err := gtserror.Newf("cannot resolve vocab type %T as statusable", t)
return nil, gtserror.SetWrongType(err) return nil, gtserror.SetWrongType(err)
} }
NormalizeIncomingContent(statusable, rawStatusable) if pollable, ok := ToPollable(statusable); ok {
NormalizeIncomingAttachments(statusable, rawStatusable) // Question requires extra normalization, and
NormalizeIncomingSummary(statusable, rawStatusable) // fortunately directly implements Statusable.
NormalizeIncomingName(statusable, rawStatusable) NormalizeIncomingPollOptions(pollable, raw)
statusable = pollable
}
NormalizeIncomingContent(statusable, raw)
NormalizeIncomingAttachments(statusable, raw)
NormalizeIncomingSummary(statusable, raw)
NormalizeIncomingName(statusable, raw)
// Release.
putMap(raw)
return statusable, nil return statusable, nil
} }
@ -85,40 +157,32 @@ func ResolveStatusable(ctx context.Context, b []byte) (Statusable, error) {
// //
// Works for: Application, Group, Organization, Person, Service // Works for: Application, Group, Organization, Person, Service
func ResolveAccountable(ctx context.Context, b []byte) (Accountable, error) { func ResolveAccountable(ctx context.Context, b []byte) (Accountable, error) {
rawAccountable := make(map[string]interface{}) // Get "raw" map
if err := json.Unmarshal(b, &rawAccountable); err != nil { // destination.
raw := getMap()
// Unmarshal the raw JSON data in a "raw" JSON map.
if err := json.Unmarshal(b, &raw); err != nil {
return nil, gtserror.Newf("error unmarshalling bytes into json: %w", err) return nil, gtserror.Newf("error unmarshalling bytes into json: %w", err)
} }
t, err := streams.ToType(ctx, rawAccountable) // Resolve an ActivityStreams type from JSON.
t, err := streams.ToType(ctx, raw)
if err != nil { if err != nil {
return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err) return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err)
} }
var ( // Attempt to cast as Statusable.
accountable Accountable accountable, ok := ToAccountable(t)
ok bool
)
switch t.GetTypeName() {
case ActorApplication:
accountable, ok = t.(vocab.ActivityStreamsApplication)
case ActorGroup:
accountable, ok = t.(vocab.ActivityStreamsGroup)
case ActorOrganization:
accountable, ok = t.(vocab.ActivityStreamsOrganization)
case ActorPerson:
accountable, ok = t.(vocab.ActivityStreamsPerson)
case ActorService:
accountable, ok = t.(vocab.ActivityStreamsService)
}
if !ok { if !ok {
err = gtserror.Newf("could not resolve %T to Accountable", t) err := gtserror.Newf("cannot resolve vocab type %T as accountable", t)
return nil, gtserror.SetWrongType(err) return nil, gtserror.SetWrongType(err)
} }
NormalizeIncomingSummary(accountable, rawAccountable) NormalizeIncomingSummary(accountable, raw)
// Release.
putMap(raw)
return accountable, nil return accountable, nil
} }

View file

@ -43,7 +43,7 @@ func (suite *ResolveTestSuite) TestResolveDocumentAsAccountable() {
accountable, err := ap.ResolveAccountable(context.Background(), b) accountable, err := ap.ResolveAccountable(context.Background(), b)
suite.True(gtserror.WrongType(err)) suite.True(gtserror.WrongType(err))
suite.EqualError(err, "ResolveAccountable: could not resolve *typedocument.ActivityStreamsDocument to Accountable") suite.EqualError(err, "ResolveAccountable: cannot resolve vocab type *typedocument.ActivityStreamsDocument as accountable")
suite.Nil(accountable) suite.Nil(accountable)
} }

View file

@ -486,7 +486,7 @@ func (suite *InboxPostTestSuite) TestPostEmptyCreate() {
requestingAccount, requestingAccount,
targetAccount, targetAccount,
http.StatusBadRequest, http.StatusBadRequest,
`{"error":"Bad Request: incoming Activity Create did not have required id property set"}`, `{"error":"Bad Request: missing ActivityStreams id property"}`,
suite.signatureCheck, suite.signatureCheck,
) )
} }
@ -511,7 +511,7 @@ func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() {
requestingAccount, requestingAccount,
targetAccount, targetAccount,
http.StatusForbidden, http.StatusForbidden,
`{"error":"Forbidden"}`, `{"error":"Forbidden: blocked"}`,
suite.signatureCheck, suite.signatureCheck,
) )
} }
@ -555,7 +555,7 @@ func (suite *InboxPostTestSuite) TestPostUnauthorized() {
requestingAccount, requestingAccount,
targetAccount, targetAccount,
http.StatusUnauthorized, http.StatusUnauthorized,
`{"error":"Unauthorized"}`, `{"error":"Unauthorized: not authenticated"}`,
// Omit signature check middleware. // Omit signature check middleware.
) )
} }

View file

@ -19,10 +19,8 @@
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@ -30,7 +28,6 @@
errorsv2 "codeberg.org/gruf/go-errors/v2" errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-kv" "codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/activity/pub" "github.com/superseriousbusiness/activity/pub"
"github.com/superseriousbusiness/activity/streams"
"github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
@ -132,12 +129,13 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// Authenticate request by checking http signature. // Authenticate request by checking http signature.
ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r) ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
if err != nil { if err != nil {
err := gtserror.Newf("error authenticating post inbox: %w", err)
return false, gtserror.NewErrorInternalError(err) return false, gtserror.NewErrorInternalError(err)
} }
if !authenticated { if !authenticated {
err = errors.New("not authenticated") const text = "not authenticated"
return false, gtserror.NewErrorUnauthorized(err) return false, gtserror.NewErrorUnauthorized(errors.New(text), text)
} }
/* /*
@ -146,7 +144,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
*/ */
// Obtain the activity; reject unknown activities. // Obtain the activity; reject unknown activities.
activity, errWithCode := resolveActivity(ctx, r) activity, errWithCode := ap.ResolveIncomingActivity(r)
if errWithCode != nil { if errWithCode != nil {
return false, errWithCode return false, errWithCode
} }
@ -156,6 +154,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// involved in it tangentially. // involved in it tangentially.
ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity) ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
if err != nil { if err != nil {
err := gtserror.Newf("error during post inbox request body hook: %w", err)
return false, gtserror.NewErrorInternalError(err) return false, gtserror.NewErrorInternalError(err)
} }
@ -174,6 +173,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
} }
// Real error has occurred. // Real error has occurred.
err := gtserror.Newf("error authorizing post inbox: %w", err)
return false, gtserror.NewErrorInternalError(err) return false, gtserror.NewErrorInternalError(err)
} }
@ -181,8 +181,8 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// Block exists either from this instance against // Block exists either from this instance against
// one or more directly involved actors, or between // one or more directly involved actors, or between
// receiving account and one of those actors. // receiving account and one of those actors.
err = errors.New("blocked") const text = "blocked"
return false, gtserror.NewErrorForbidden(err) return false, gtserror.NewErrorForbidden(errors.New(text), text)
} }
// Copy existing URL + add request host and scheme. // Copy existing URL + add request host and scheme.
@ -205,13 +205,13 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// Send the rejection to the peer. // Send the rejection to the peer.
if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) { if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) {
// Log the original error but return something a bit more generic. // Log the original error but return something a bit more generic.
l.Debugf("malformed incoming Activity: %q", err) log.Warnf(ctx, "malformed incoming activity: %v", err)
err = errors.New("malformed incoming Activity: an Object and/or Target was required but not set") const text = "malformed activity: missing Object and / or Target"
return false, gtserror.NewErrorBadRequest(err, err.Error()) return false, gtserror.NewErrorBadRequest(errors.New(text), text)
} }
// There's been some real error. // There's been some real error.
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err) err := gtserror.Newf("error calling sideEffectActor.PostInbox: %w", err)
return false, gtserror.NewErrorInternalError(err) return false, gtserror.NewErrorInternalError(err)
} }
@ -241,7 +241,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
) { ) {
// Failed inbox forwarding is not a show-stopper, // Failed inbox forwarding is not a show-stopper,
// and doesn't even necessarily denote a real error. // and doesn't even necessarily denote a real error.
l.Warnf("error calling sideEffectActor.InboxForwarding: %q", err) l.Warnf("error calling sideEffectActor.InboxForwarding: %v", err)
} }
} }
@ -250,58 +250,6 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
return true, nil return true, nil
} }
// resolveActivity is a util function for pulling a
// pub.Activity type out of an incoming POST request.
func resolveActivity(ctx context.Context, r *http.Request) (pub.Activity, gtserror.WithCode) {
// Tidy up when done.
defer r.Body.Close()
b, err := io.ReadAll(r.Body)
if err != nil {
err = fmt.Errorf("error reading request body: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
var rawActivity map[string]interface{}
if err := json.Unmarshal(b, &rawActivity); err != nil {
err = fmt.Errorf("error unmarshalling request body: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
t, err := streams.ToType(ctx, rawActivity)
if err != nil {
if !streams.IsUnmatchedErr(err) {
// Real error.
err = fmt.Errorf("error matching json to type: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Respond with bad request; we just couldn't
// match the type to one that we know about.
err = errors.New("body json could not be resolved to ActivityStreams value")
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
activity, ok := t.(pub.Activity)
if !ok {
err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t)
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
if activity.GetJSONLDId() == nil {
err = fmt.Errorf("incoming Activity %s did not have required id property set", activity.GetTypeName())
return nil, gtserror.NewErrorBadRequest(err, err.Error())
}
// 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
// Likewise, if it's an Accountable, we'll normalize some fields on it.
ap.NormalizeIncomingActivityObject(activity, rawActivity)
return activity, nil
}
/* /*
Functions below are just lightly wrapped versions Functions below are just lightly wrapped versions
of the original go-fed federatingActor functions. of the original go-fed federatingActor functions.