[chore] internal/ap: add pollable AS types, code reformatting, general niceties (#2248)
This commit is contained in:
parent
a1ab2c255a
commit
297b6eeaaa
|
@ -78,3 +78,49 @@ const (
|
|||
// and https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,12 +28,53 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
"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
|
||||
// an interface's preferredUsername property. Will return an
|
||||
// error if preferredUsername is nil, not a string, or empty.
|
||||
|
@ -497,6 +538,38 @@ func ExtractContent(i WithContent) string {
|
|||
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
|
||||
// (just remote URL, description, and blurhash) from the given
|
||||
// Attachmentable interface, or an error if no remote URL is set.
|
||||
|
@ -913,6 +986,52 @@ func ExtractSharedInbox(withEndpoints WithEndpoints) *url.URL {
|
|||
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
|
||||
// uris slice equals the activitystreams public uri.
|
||||
func isPublic(uris []*url.URL) bool {
|
||||
|
|
|
@ -23,11 +23,76 @@ import (
|
|||
"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'.
|
||||
// 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 {
|
||||
WithJSONLDId
|
||||
WithTypeName
|
||||
vocab.Type
|
||||
|
||||
WithPreferredUsername
|
||||
WithIcon
|
||||
|
@ -35,7 +100,6 @@ type Accountable interface {
|
|||
WithImage
|
||||
WithSummary
|
||||
WithAttachment
|
||||
WithSetSummary
|
||||
WithDiscoverable
|
||||
WithURL
|
||||
WithPublicKey
|
||||
|
@ -50,15 +114,13 @@ type Accountable interface {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
WithJSONLDId
|
||||
WithTypeName
|
||||
vocab.Type
|
||||
|
||||
WithSummary
|
||||
WithSetSummary
|
||||
WithName
|
||||
WithSetName
|
||||
WithInReplyTo
|
||||
WithPublished
|
||||
WithURL
|
||||
|
@ -68,20 +130,40 @@ type Statusable interface {
|
|||
WithSensitive
|
||||
WithConversation
|
||||
WithContent
|
||||
WithSetContent
|
||||
WithAttachment
|
||||
WithTag
|
||||
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
|
||||
type Attachmentable interface {
|
||||
WithTypeName
|
||||
WithMediaType
|
||||
WithURL
|
||||
WithName
|
||||
WithSetName
|
||||
WithBlurhash
|
||||
}
|
||||
|
||||
|
@ -160,8 +242,7 @@ type ReplyToable interface {
|
|||
// CollectionPageIterator represents the minimum interface for interacting with a wrapped
|
||||
// CollectionPage or OrderedCollectionPage in order to access both next / prev pages and items.
|
||||
type CollectionPageIterator interface {
|
||||
WithJSONLDId
|
||||
WithTypeName
|
||||
vocab.Type
|
||||
|
||||
NextPage() WithIRI
|
||||
PrevPage() WithIRI
|
||||
|
@ -189,12 +270,14 @@ type Flaggable interface {
|
|||
// WithJSONLDId represents an activity with JSONLDIdProperty.
|
||||
type WithJSONLDId interface {
|
||||
GetJSONLDId() vocab.JSONLDIdProperty
|
||||
SetJSONLDId(vocab.JSONLDIdProperty)
|
||||
}
|
||||
|
||||
// WithIRI represents an object (possibly) representable as an IRI.
|
||||
type WithIRI interface {
|
||||
GetIRI() *url.URL
|
||||
IsIRI() bool
|
||||
SetIRI(*url.URL)
|
||||
}
|
||||
|
||||
// WithType ...
|
||||
|
@ -210,20 +293,18 @@ type WithTypeName interface {
|
|||
// WithPreferredUsername represents an activity with ActivityStreamsPreferredUsernameProperty
|
||||
type WithPreferredUsername interface {
|
||||
GetActivityStreamsPreferredUsername() vocab.ActivityStreamsPreferredUsernameProperty
|
||||
SetActivityStreamsPreferredUsername(vocab.ActivityStreamsPreferredUsernameProperty)
|
||||
}
|
||||
|
||||
// WithIcon represents an activity with ActivityStreamsIconProperty
|
||||
type WithIcon interface {
|
||||
GetActivityStreamsIcon() vocab.ActivityStreamsIconProperty
|
||||
SetActivityStreamsIcon(vocab.ActivityStreamsIconProperty)
|
||||
}
|
||||
|
||||
// WithName represents an activity with ActivityStreamsNameProperty
|
||||
type WithName interface {
|
||||
GetActivityStreamsName() vocab.ActivityStreamsNameProperty
|
||||
}
|
||||
|
||||
// WithSetName represents an activity with a settable ActivityStreamsNameProperty
|
||||
type WithSetName interface {
|
||||
SetActivityStreamsName(vocab.ActivityStreamsNameProperty)
|
||||
}
|
||||
|
||||
|
@ -235,81 +316,91 @@ type WithImage interface {
|
|||
// WithSummary represents an activity with ActivityStreamsSummaryProperty
|
||||
type WithSummary interface {
|
||||
GetActivityStreamsSummary() vocab.ActivityStreamsSummaryProperty
|
||||
}
|
||||
|
||||
// WithSetSummary represents an activity that can have summary set on it.
|
||||
type WithSetSummary interface {
|
||||
SetActivityStreamsSummary(vocab.ActivityStreamsSummaryProperty)
|
||||
}
|
||||
|
||||
// WithDiscoverable represents an activity with TootDiscoverableProperty
|
||||
type WithDiscoverable interface {
|
||||
GetTootDiscoverable() vocab.TootDiscoverableProperty
|
||||
SetTootDiscoverable(vocab.TootDiscoverableProperty)
|
||||
}
|
||||
|
||||
// WithURL represents an activity with ActivityStreamsUrlProperty
|
||||
type WithURL interface {
|
||||
GetActivityStreamsUrl() vocab.ActivityStreamsUrlProperty
|
||||
SetActivityStreamsUrl(vocab.ActivityStreamsUrlProperty)
|
||||
}
|
||||
|
||||
// WithPublicKey represents an activity with W3IDSecurityV1PublicKeyProperty
|
||||
type WithPublicKey interface {
|
||||
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
||||
SetW3IDSecurityV1PublicKey(vocab.W3IDSecurityV1PublicKeyProperty)
|
||||
}
|
||||
|
||||
// WithInbox represents an activity with ActivityStreamsInboxProperty
|
||||
type WithInbox interface {
|
||||
GetActivityStreamsInbox() vocab.ActivityStreamsInboxProperty
|
||||
SetActivityStreamsInbox(vocab.ActivityStreamsInboxProperty)
|
||||
}
|
||||
|
||||
// WithOutbox represents an activity with ActivityStreamsOutboxProperty
|
||||
type WithOutbox interface {
|
||||
GetActivityStreamsOutbox() vocab.ActivityStreamsOutboxProperty
|
||||
SetActivityStreamsOutbox(vocab.ActivityStreamsOutboxProperty)
|
||||
}
|
||||
|
||||
// WithFollowing represents an activity with ActivityStreamsFollowingProperty
|
||||
type WithFollowing interface {
|
||||
GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty
|
||||
SetActivityStreamsFollowing(vocab.ActivityStreamsFollowingProperty)
|
||||
}
|
||||
|
||||
// WithFollowers represents an activity with ActivityStreamsFollowersProperty
|
||||
type WithFollowers interface {
|
||||
GetActivityStreamsFollowers() vocab.ActivityStreamsFollowersProperty
|
||||
SetActivityStreamsFollowers(vocab.ActivityStreamsFollowersProperty)
|
||||
}
|
||||
|
||||
// WithFeatured represents an activity with TootFeaturedProperty
|
||||
type WithFeatured interface {
|
||||
GetTootFeatured() vocab.TootFeaturedProperty
|
||||
SetTootFeatured(vocab.TootFeaturedProperty)
|
||||
}
|
||||
|
||||
// WithAttributedTo represents an activity with ActivityStreamsAttributedToProperty
|
||||
type WithAttributedTo interface {
|
||||
GetActivityStreamsAttributedTo() vocab.ActivityStreamsAttributedToProperty
|
||||
SetActivityStreamsAttributedTo(vocab.ActivityStreamsAttributedToProperty)
|
||||
}
|
||||
|
||||
// WithAttachment represents an activity with ActivityStreamsAttachmentProperty
|
||||
type WithAttachment interface {
|
||||
GetActivityStreamsAttachment() vocab.ActivityStreamsAttachmentProperty
|
||||
SetActivityStreamsAttachment(vocab.ActivityStreamsAttachmentProperty)
|
||||
}
|
||||
|
||||
// WithTo represents an activity with ActivityStreamsToProperty
|
||||
type WithTo interface {
|
||||
GetActivityStreamsTo() vocab.ActivityStreamsToProperty
|
||||
SetActivityStreamsTo(vocab.ActivityStreamsToProperty)
|
||||
}
|
||||
|
||||
// WithInReplyTo represents an activity with ActivityStreamsInReplyToProperty
|
||||
type WithInReplyTo interface {
|
||||
GetActivityStreamsInReplyTo() vocab.ActivityStreamsInReplyToProperty
|
||||
SetActivityStreamsInReplyTo(vocab.ActivityStreamsInReplyToProperty)
|
||||
}
|
||||
|
||||
// WithCC represents an activity with ActivityStreamsCcProperty
|
||||
type WithCC interface {
|
||||
GetActivityStreamsCc() vocab.ActivityStreamsCcProperty
|
||||
SetActivityStreamsCc(vocab.ActivityStreamsCcProperty)
|
||||
}
|
||||
|
||||
// WithSensitive represents an activity with ActivityStreamsSensitiveProperty
|
||||
type WithSensitive interface {
|
||||
GetActivityStreamsSensitive() vocab.ActivityStreamsSensitiveProperty
|
||||
SetActivityStreamsSensitive(vocab.ActivityStreamsSensitiveProperty)
|
||||
}
|
||||
|
||||
// WithConversation ...
|
||||
|
@ -319,36 +410,37 @@ type WithConversation interface { // TODO
|
|||
// WithContent represents an activity with ActivityStreamsContentProperty
|
||||
type WithContent interface {
|
||||
GetActivityStreamsContent() vocab.ActivityStreamsContentProperty
|
||||
}
|
||||
|
||||
// WithSetContent represents an activity that can have content set on it.
|
||||
type WithSetContent interface {
|
||||
SetActivityStreamsContent(vocab.ActivityStreamsContentProperty)
|
||||
}
|
||||
|
||||
// WithPublished represents an activity with ActivityStreamsPublishedProperty
|
||||
type WithPublished interface {
|
||||
GetActivityStreamsPublished() vocab.ActivityStreamsPublishedProperty
|
||||
SetActivityStreamsPublished(vocab.ActivityStreamsPublishedProperty)
|
||||
}
|
||||
|
||||
// WithTag represents an activity with ActivityStreamsTagProperty
|
||||
type WithTag interface {
|
||||
GetActivityStreamsTag() vocab.ActivityStreamsTagProperty
|
||||
SetActivityStreamsTag(vocab.ActivityStreamsTagProperty)
|
||||
}
|
||||
|
||||
// WithReplies represents an activity with ActivityStreamsRepliesProperty
|
||||
type WithReplies interface {
|
||||
GetActivityStreamsReplies() vocab.ActivityStreamsRepliesProperty
|
||||
SetActivityStreamsReplies(vocab.ActivityStreamsRepliesProperty)
|
||||
}
|
||||
|
||||
// WithMediaType represents an activity with ActivityStreamsMediaTypeProperty
|
||||
type WithMediaType interface {
|
||||
GetActivityStreamsMediaType() vocab.ActivityStreamsMediaTypeProperty
|
||||
SetActivityStreamsMediaType(vocab.ActivityStreamsMediaTypeProperty)
|
||||
}
|
||||
|
||||
// WithBlurhash represents an activity with TootBlurhashProperty
|
||||
type WithBlurhash interface {
|
||||
GetTootBlurhash() vocab.TootBlurhashProperty
|
||||
SetTootBlurhash(vocab.TootBlurhashProperty)
|
||||
}
|
||||
|
||||
// type withFocalPoint interface {
|
||||
|
@ -358,44 +450,83 @@ type WithBlurhash interface {
|
|||
// WithHref represents an activity with ActivityStreamsHrefProperty
|
||||
type WithHref interface {
|
||||
GetActivityStreamsHref() vocab.ActivityStreamsHrefProperty
|
||||
SetActivityStreamsHref(vocab.ActivityStreamsHrefProperty)
|
||||
}
|
||||
|
||||
// WithUpdated represents an activity with ActivityStreamsUpdatedProperty
|
||||
type WithUpdated interface {
|
||||
GetActivityStreamsUpdated() vocab.ActivityStreamsUpdatedProperty
|
||||
SetActivityStreamsUpdated(vocab.ActivityStreamsUpdatedProperty)
|
||||
}
|
||||
|
||||
// WithActor represents an activity with ActivityStreamsActorProperty
|
||||
type WithActor interface {
|
||||
GetActivityStreamsActor() vocab.ActivityStreamsActorProperty
|
||||
SetActivityStreamsActor(vocab.ActivityStreamsActorProperty)
|
||||
}
|
||||
|
||||
// WithObject represents an activity with ActivityStreamsObjectProperty
|
||||
type WithObject interface {
|
||||
GetActivityStreamsObject() vocab.ActivityStreamsObjectProperty
|
||||
SetActivityStreamsObject(vocab.ActivityStreamsObjectProperty)
|
||||
}
|
||||
|
||||
// WithNext represents an activity with ActivityStreamsNextProperty
|
||||
type WithNext interface {
|
||||
GetActivityStreamsNext() vocab.ActivityStreamsNextProperty
|
||||
SetActivityStreamsNext(vocab.ActivityStreamsNextProperty)
|
||||
}
|
||||
|
||||
// WithPartOf represents an activity with ActivityStreamsPartOfProperty
|
||||
type WithPartOf interface {
|
||||
GetActivityStreamsPartOf() vocab.ActivityStreamsPartOfProperty
|
||||
SetActivityStreamsPartOf(vocab.ActivityStreamsPartOfProperty)
|
||||
}
|
||||
|
||||
// WithItems represents an activity with ActivityStreamsItemsProperty
|
||||
type WithItems interface {
|
||||
GetActivityStreamsItems() vocab.ActivityStreamsItemsProperty
|
||||
SetActivityStreamsItems(vocab.ActivityStreamsItemsProperty)
|
||||
}
|
||||
|
||||
// WithManuallyApprovesFollowers represents a Person or profile with the ManuallyApprovesFollowers property.
|
||||
type WithManuallyApprovesFollowers interface {
|
||||
GetActivityStreamsManuallyApprovesFollowers() vocab.ActivityStreamsManuallyApprovesFollowersProperty
|
||||
SetActivityStreamsManuallyApprovesFollowers(vocab.ActivityStreamsManuallyApprovesFollowersProperty)
|
||||
}
|
||||
|
||||
// WithEndpoints represents a Person or profile with the endpoints property
|
||||
type WithEndpoints interface {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -37,92 +37,62 @@ import (
|
|||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
withObject, ok := activity.(WithObject)
|
||||
func NormalizeIncomingActivity(activity pub.Activity, rawJSON map[string]interface{}) {
|
||||
// From the activity extract the data vocab.Type + its "raw" JSON.
|
||||
dataType, rawData, ok := ExtractActivityData(activity, rawJSON)
|
||||
if !ok {
|
||||
// Create was not a WithObject.
|
||||
return
|
||||
}
|
||||
|
||||
createObject := withObject.GetActivityStreamsObject()
|
||||
if createObject == nil {
|
||||
// No object set.
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
switch dataType.GetTypeName() {
|
||||
// "Pollable" types.
|
||||
case ActivityQuestion:
|
||||
pollable, ok := dataType.(Pollable)
|
||||
if !ok {
|
||||
// Object is not Statusable;
|
||||
// we're not interested.
|
||||
return
|
||||
}
|
||||
|
||||
rawObject, ok := rawJSON["object"]
|
||||
if !ok {
|
||||
// No object in raw map.
|
||||
return
|
||||
}
|
||||
// Normalize the Pollable specific properties.
|
||||
NormalizeIncomingPollOptions(pollable, rawData)
|
||||
|
||||
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 {
|
||||
// Object wasn't a json object.
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
NormalizeIncomingContent(statusable, rawData)
|
||||
NormalizeIncomingAttachments(statusable, rawData)
|
||||
NormalizeIncomingSummary(statusable, rawData)
|
||||
NormalizeIncomingName(statusable, rawData)
|
||||
|
||||
rawObject, ok := rawJSON["object"]
|
||||
// "Accountable" types.
|
||||
case ActorApplication,
|
||||
ActorGroup,
|
||||
ActorOrganization,
|
||||
ActorPerson,
|
||||
ActorService:
|
||||
accountable, ok := dataType.(Accountable)
|
||||
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)
|
||||
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
|
||||
// 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"]
|
||||
if !ok {
|
||||
// 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
|
||||
// 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"]
|
||||
if !ok {
|
||||
// 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
|
||||
// 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"]
|
||||
if !ok {
|
||||
// No name in rawJSON.
|
||||
|
@ -284,3 +254,60 @@ func NormalizeIncomingName(item WithSetName, rawJSON map[string]interface{}) {
|
|||
nameProp.AppendXMLSchemaString(name)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -191,7 +191,7 @@ func (suite *NormalizeTestSuite) TestNormalizeActivityObject() {
|
|||
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))
|
||||
}
|
||||
|
||||
|
|
|
@ -20,62 +20,134 @@ package ap
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"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.
|
||||
// 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) {
|
||||
rawStatusable := make(map[string]interface{})
|
||||
if err := json.Unmarshal(b, &rawStatusable); err != nil {
|
||||
// Get "raw" map
|
||||
// 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)
|
||||
}
|
||||
|
||||
t, err := streams.ToType(ctx, rawStatusable)
|
||||
// Resolve an ActivityStreams type from JSON.
|
||||
t, err := streams.ToType(ctx, raw)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
statusable Statusable
|
||||
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)
|
||||
}
|
||||
|
||||
// Attempt to cast as Statusable.
|
||||
statusable, ok := ToStatusable(t)
|
||||
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)
|
||||
}
|
||||
|
||||
NormalizeIncomingContent(statusable, rawStatusable)
|
||||
NormalizeIncomingAttachments(statusable, rawStatusable)
|
||||
NormalizeIncomingSummary(statusable, rawStatusable)
|
||||
NormalizeIncomingName(statusable, rawStatusable)
|
||||
if pollable, ok := ToPollable(statusable); ok {
|
||||
// Question requires extra normalization, and
|
||||
// fortunately directly implements Statusable.
|
||||
NormalizeIncomingPollOptions(pollable, raw)
|
||||
statusable = pollable
|
||||
}
|
||||
|
||||
NormalizeIncomingContent(statusable, raw)
|
||||
NormalizeIncomingAttachments(statusable, raw)
|
||||
NormalizeIncomingSummary(statusable, raw)
|
||||
NormalizeIncomingName(statusable, raw)
|
||||
|
||||
// Release.
|
||||
putMap(raw)
|
||||
|
||||
return statusable, nil
|
||||
}
|
||||
|
@ -85,40 +157,32 @@ func ResolveStatusable(ctx context.Context, b []byte) (Statusable, error) {
|
|||
//
|
||||
// Works for: Application, Group, Organization, Person, Service
|
||||
func ResolveAccountable(ctx context.Context, b []byte) (Accountable, error) {
|
||||
rawAccountable := make(map[string]interface{})
|
||||
if err := json.Unmarshal(b, &rawAccountable); err != nil {
|
||||
// Get "raw" map
|
||||
// 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)
|
||||
}
|
||||
|
||||
t, err := streams.ToType(ctx, rawAccountable)
|
||||
// Resolve an ActivityStreams type from JSON.
|
||||
t, err := streams.ToType(ctx, raw)
|
||||
if err != nil {
|
||||
return nil, gtserror.Newf("error resolving json into ap vocab type: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
accountable Accountable
|
||||
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)
|
||||
}
|
||||
|
||||
// Attempt to cast as Statusable.
|
||||
accountable, ok := ToAccountable(t)
|
||||
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)
|
||||
}
|
||||
|
||||
NormalizeIncomingSummary(accountable, rawAccountable)
|
||||
NormalizeIncomingSummary(accountable, raw)
|
||||
|
||||
// Release.
|
||||
putMap(raw)
|
||||
|
||||
return accountable, nil
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func (suite *ResolveTestSuite) TestResolveDocumentAsAccountable() {
|
|||
|
||||
accountable, err := ap.ResolveAccountable(context.Background(), b)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -486,7 +486,7 @@ func (suite *InboxPostTestSuite) TestPostEmptyCreate() {
|
|||
requestingAccount,
|
||||
targetAccount,
|
||||
http.StatusBadRequest,
|
||||
`{"error":"Bad Request: incoming Activity Create did not have required id property set"}`,
|
||||
`{"error":"Bad Request: missing ActivityStreams id property"}`,
|
||||
suite.signatureCheck,
|
||||
)
|
||||
}
|
||||
|
@ -511,7 +511,7 @@ func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() {
|
|||
requestingAccount,
|
||||
targetAccount,
|
||||
http.StatusForbidden,
|
||||
`{"error":"Forbidden"}`,
|
||||
`{"error":"Forbidden: blocked"}`,
|
||||
suite.signatureCheck,
|
||||
)
|
||||
}
|
||||
|
@ -555,7 +555,7 @@ func (suite *InboxPostTestSuite) TestPostUnauthorized() {
|
|||
requestingAccount,
|
||||
targetAccount,
|
||||
http.StatusUnauthorized,
|
||||
`{"error":"Unauthorized"}`,
|
||||
`{"error":"Unauthorized: not authenticated"}`,
|
||||
// Omit signature check middleware.
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,10 +19,8 @@ package federation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
@ -30,7 +28,6 @@ import (
|
|||
errorsv2 "codeberg.org/gruf/go-errors/v2"
|
||||
"codeberg.org/gruf/go-kv"
|
||||
"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/db"
|
||||
|
@ -132,12 +129,13 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
|
|||
// Authenticate request by checking http signature.
|
||||
ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error authenticating post inbox: %w", err)
|
||||
return false, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !authenticated {
|
||||
err = errors.New("not authenticated")
|
||||
return false, gtserror.NewErrorUnauthorized(err)
|
||||
const text = "not authenticated"
|
||||
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.
|
||||
activity, errWithCode := resolveActivity(ctx, r)
|
||||
activity, errWithCode := ap.ResolveIncomingActivity(r)
|
||||
if errWithCode != nil {
|
||||
return false, errWithCode
|
||||
}
|
||||
|
@ -156,6 +154,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
|
|||
// involved in it tangentially.
|
||||
ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error during post inbox request body hook: %w", err)
|
||||
return false, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
|
@ -174,6 +173,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
|
|||
}
|
||||
|
||||
// Real error has occurred.
|
||||
err := gtserror.Newf("error authorizing post inbox: %w", 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
|
||||
// one or more directly involved actors, or between
|
||||
// receiving account and one of those actors.
|
||||
err = errors.New("blocked")
|
||||
return false, gtserror.NewErrorForbidden(err)
|
||||
const text = "blocked"
|
||||
return false, gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// 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.
|
||||
if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) {
|
||||
// Log the original error but return something a bit more generic.
|
||||
l.Debugf("malformed incoming Activity: %q", err)
|
||||
err = errors.New("malformed incoming Activity: an Object and/or Target was required but not set")
|
||||
return false, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
log.Warnf(ctx, "malformed incoming activity: %v", err)
|
||||
const text = "malformed activity: missing Object and / or Target"
|
||||
return false, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -241,7 +241,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
|
|||
) {
|
||||
// Failed inbox forwarding is not a show-stopper,
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
of the original go-fed federatingActor functions.
|
||||
|
|
Loading…
Reference in New Issue