diff options
author | 2023-11-04 20:21:20 +0000 | |
---|---|---|
committer | 2023-11-04 20:21:20 +0000 | |
commit | 41435a6c4ee0a5b52528890edf3fbf5a9dc0a6c8 (patch) | |
tree | 987b5d7787b24f6f6e340bbcf7fd1b052fe40dfc /internal/ap | |
parent | [docs/bugfix] fix link to swagger yaml (#2333) (diff) | |
download | gotosocial-41435a6c4ee0a5b52528890edf3fbf5a9dc0a6c8.tar.xz |
[feature] support canceling scheduled tasks, some federation API performance improvements (#2329)
Diffstat (limited to 'internal/ap')
-rw-r--r-- | internal/ap/extract.go | 101 | ||||
-rw-r--r-- | internal/ap/interfaces.go | 98 | ||||
-rw-r--r-- | internal/ap/properties.go | 325 |
3 files changed, 510 insertions, 14 deletions
diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 74c4497b3..332550578 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -91,6 +91,105 @@ func ExtractActivityData(activity pub.Activity, rawJSON map[string]any) ([]TypeO } } +// ExtractAccountables extracts Accountable objects from a slice TypeOrIRI, returning extracted and remaining TypeOrIRIs. +func ExtractAccountables(arr []TypeOrIRI) ([]Accountable, []TypeOrIRI) { + var accounts []Accountable + + for i := 0; i < len(arr); i++ { + elem := arr[i] + + if elem.IsIRI() { + // skip IRIs + continue + } + + // Extract AS vocab type + // associated with elem. + t := elem.GetType() + + // Try cast AS type as Accountable. + account, ok := ToAccountable(t) + if !ok { + continue + } + + // Add casted accountable type. + accounts = append(accounts, account) + + // Drop elem from slice. + copy(arr[:i], arr[i+1:]) + arr = arr[:len(arr)-1] + } + + return accounts, arr +} + +// ExtractStatusables extracts Statusable objects from a slice TypeOrIRI, returning extracted and remaining TypeOrIRIs. +func ExtractStatusables(arr []TypeOrIRI) ([]Statusable, []TypeOrIRI) { + var statuses []Statusable + + for i := 0; i < len(arr); i++ { + elem := arr[i] + + if elem.IsIRI() { + // skip IRIs + continue + } + + // Extract AS vocab type + // associated with elem. + t := elem.GetType() + + // Try cast AS type as Statusable. + status, ok := ToStatusable(t) + if !ok { + continue + } + + // Add casted Statusable type. + statuses = append(statuses, status) + + // Drop elem from slice. + copy(arr[:i], arr[i+1:]) + arr = arr[:len(arr)-1] + } + + return statuses, arr +} + +// ExtractPollOptionables extracts PollOptionable objects from a slice TypeOrIRI, returning extracted and remaining TypeOrIRIs. +func ExtractPollOptionables(arr []TypeOrIRI) ([]PollOptionable, []TypeOrIRI) { + var options []PollOptionable + + for i := 0; i < len(arr); i++ { + elem := arr[i] + + if elem.IsIRI() { + // skip IRIs + continue + } + + // Extract AS vocab type + // associated with elem. + t := elem.GetType() + + // Try cast as PollOptionable. + option, ok := ToPollOptionable(t) + if !ok { + continue + } + + // Add casted PollOptionable type. + options = append(options, option) + + // Drop elem from slice. + copy(arr[:i], arr[i+1:]) + arr = arr[:len(arr)-1] + } + + return options, arr +} + // ExtractPreferredUsername returns a string representation of // an interface's preferredUsername property. Will return an // error if preferredUsername is nil, not a string, or empty. @@ -192,7 +291,7 @@ func ExtractToURIs(i WithTo) []*url.URL { // ExtractCcURIs returns a slice of URIs // that the given WithCC addresses as Cc. -func ExtractCcURIs(i WithCC) []*url.URL { +func ExtractCcURIs(i WithCc) []*url.URL { ccProp := i.GetActivityStreamsCc() if ccProp == nil { return nil diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 6ba3c3735..fed69d69d 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -23,6 +23,21 @@ import ( "github.com/superseriousbusiness/activity/streams/vocab" ) +// IsActivityable returns whether AS vocab type name is acceptable as Activityable. +func IsActivityable(typeName string) bool { + return isActivity(typeName) || + isIntransitiveActivity(typeName) +} + +// ToActivityable safely tries to cast vocab.Type as Activityable, also checking for expected AS type names. +func ToActivityable(t vocab.Type) (Activityable, bool) { + activityable, ok := t.(Activityable) + if !ok || !IsActivityable(t.GetTypeName()) { + return nil, false + } + return activityable, true +} + // IsAccountable returns whether AS vocab type name is acceptable as Accountable. func IsAccountable(typeName string) bool { switch typeName { @@ -88,6 +103,43 @@ func ToPollable(t vocab.Type) (Pollable, bool) { return pollable, true } +// IsPollOptionable returns whether AS vocab type name is acceptable as PollOptionable. +func IsPollOptionable(typeName string) bool { + return typeName == ObjectNote +} + +// ToPollOptionable safely tries to cast vocab.Type as PollOptionable, also checking for expected AS type names. +func ToPollOptionable(t vocab.Type) (PollOptionable, bool) { + note, ok := t.(vocab.ActivityStreamsNote) + if !ok || !IsPollOptionable(t.GetTypeName()) { + return nil, false + } + if note.GetActivityStreamsContent() != nil || + note.GetActivityStreamsName() == nil { + // A PollOption is an ActivityStreamsNote + // WITHOUT a content property, instead only + // a name property. + return nil, false + } + return note, true +} + +// Activityable represents the minimum activitypub interface for representing an 'activity'. +// (see: IsActivityable() for types implementing this, though you MUST make sure to check +// the typeName as this bare interface may be implementable by non-Activityable types). +type Activityable interface { + // Activity is also a vocab.Type + vocab.Type + + WithTo + WithCc + WithBcc + WithAttributedTo + WithActor + WithObject + WithPublished +} + // Accountable represents the minimum activitypub interface for representing an 'account'. // (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). @@ -126,7 +178,7 @@ type Statusable interface { WithURL WithAttributedTo WithTo - WithCC + WithCc WithSensitive WithConversation WithContent @@ -145,16 +197,21 @@ type Pollable interface { WithClosed WithVotersCount - // base-interface + // base-interfaces Statusable } -// PollOptionable represents the minimum activitypub interface for representing a poll 'option'. -// (see: IsPollOptionable() for types implementing this). +// PollOptionable represents the minimum activitypub interface for representing a poll 'vote'. +// (see: IsPollOptionable() 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 PollOptionable interface { - WithTypeName + vocab.Type + WithName + WithTo + WithInReplyTo WithReplies + WithAttributedTo } // Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. (see: IsAttachmentable). @@ -226,13 +283,13 @@ type Announceable interface { WithObject WithPublished WithTo - WithCC + WithCc } // Addressable represents the minimum interface for an addressed activity. type Addressable interface { WithTo - WithCC + WithCc } // ReplyToable represents the minimum interface for an Activity that can be InReplyTo another activity. @@ -268,6 +325,15 @@ type TypeOrIRI interface { WithType } +// Property represents the minimum interface for an ActivityStreams property with IRIs. +type Property[T TypeOrIRI] interface { + Len() int + At(int) T + + AppendIRI(*url.URL) + SetIRI(int, *url.URL) +} + // WithJSONLDId represents an activity with JSONLDIdProperty. type WithJSONLDId interface { GetJSONLDId() vocab.JSONLDIdProperty @@ -386,18 +452,24 @@ type WithTo interface { SetActivityStreamsTo(vocab.ActivityStreamsToProperty) } +// WithCC represents an activity with ActivityStreamsCcProperty +type WithCc interface { + GetActivityStreamsCc() vocab.ActivityStreamsCcProperty + SetActivityStreamsCc(vocab.ActivityStreamsCcProperty) +} + +// WithCC represents an activity with ActivityStreamsBccProperty +type WithBcc interface { + GetActivityStreamsBcc() vocab.ActivityStreamsBccProperty + SetActivityStreamsBcc(vocab.ActivityStreamsBccProperty) +} + // 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 diff --git a/internal/ap/properties.go b/internal/ap/properties.go new file mode 100644 index 000000000..d8441003f --- /dev/null +++ b/internal/ap/properties.go @@ -0,0 +1,325 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <http://www.gnu.org/licenses/>. + +package ap + +import ( + "fmt" + "net/url" + "time" + + "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/activity/streams/vocab" +) + +// MustGet performs the given 'Get$Property(with) (T, error)' signature function, panicking on error. +// func MustGet[W, T any](fn func(W) (T, error), with W) T { +// t, err := fn(with) +// if err != nil { +// panicfAt(3, "error getting property on %T: %w", with, err) +// } +// return t +// } + +// MustSet performs the given 'Set$Property(with, T) error' signature function, panicking on error. +// func MustSet[W, T any](fn func(W, T) error, with W, value T) { +// err := fn(with, value) +// if err != nil { +// panicfAt(3, "error setting property on %T: %w", with, err) +// } +// } + +// AppendSet performs the given 'Append$Property(with, ...T) error' signature function, panicking on error. +// func MustAppend[W, T any](fn func(W, ...T) error, with W, values ...T) { +// err := fn(with, values...) +// if err != nil { +// panicfAt(3, "error appending properties on %T: %w", with, err) +// } +// } + +// GetJSONLDId returns the ID of 'with', or nil. +func GetJSONLDId(with WithJSONLDId) *url.URL { + idProp := with.GetJSONLDId() + if idProp == nil { + return nil + } + id := idProp.Get() + if id == nil { + return nil + } + return id +} + +// SetJSONLDId sets the given URL to the JSONLD ID of 'with'. +func SetJSONLDId(with WithJSONLDId, id *url.URL) { + idProp := with.GetJSONLDId() + if idProp == nil { + idProp = streams.NewJSONLDIdProperty() + } + idProp.SetIRI(id) + with.SetJSONLDId(idProp) +} + +// SetJSONLDIdStr sets the given string to the JSONLDID of 'with'. Returns error +func SetJSONLDIdStr(with WithJSONLDId, id string) error { + u, err := url.Parse(id) + if err != nil { + return fmt.Errorf("error parsing id url: %w", err) + } + SetJSONLDId(with, u) + return nil +} + +// GetTo returns the IRIs contained in the To property of 'with'. Panics on entries with missing ID. +func GetTo(with WithTo) []*url.URL { + toProp := with.GetActivityStreamsTo() + return getIRIs[vocab.ActivityStreamsToPropertyIterator](toProp) +} + +// AppendTo appends the given IRIs to the To property of 'with'. +func AppendTo(with WithTo, to ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsToPropertyIterator] { + toProp := with.GetActivityStreamsTo() + if toProp == nil { + toProp = streams.NewActivityStreamsToProperty() + with.SetActivityStreamsTo(toProp) + } + return toProp + }, to...) +} + +// GetCc returns the IRIs contained in the Cc property of 'with'. Panics on entries with missing ID. +func GetCc(with WithCc) []*url.URL { + ccProp := with.GetActivityStreamsCc() + return getIRIs[vocab.ActivityStreamsCcPropertyIterator](ccProp) +} + +// AppendCc appends the given IRIs to the Cc property of 'with'. +func AppendCc(with WithCc, cc ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsCcPropertyIterator] { + ccProp := with.GetActivityStreamsCc() + if ccProp == nil { + ccProp = streams.NewActivityStreamsCcProperty() + with.SetActivityStreamsCc(ccProp) + } + return ccProp + }, cc...) +} + +// GetBcc returns the IRIs contained in the Bcc property of 'with'. Panics on entries with missing ID. +func GetBcc(with WithBcc) []*url.URL { + bccProp := with.GetActivityStreamsBcc() + return getIRIs[vocab.ActivityStreamsBccPropertyIterator](bccProp) +} + +// AppendBcc appends the given IRIs to the Bcc property of 'with'. +func AppendBcc(with WithBcc, bcc ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsBccPropertyIterator] { + bccProp := with.GetActivityStreamsBcc() + if bccProp == nil { + bccProp = streams.NewActivityStreamsBccProperty() + with.SetActivityStreamsBcc(bccProp) + } + return bccProp + }, bcc...) +} + +// GetActor returns the IRIs contained in the Actor property of 'with'. Panics on entries with missing ID. +func GetActor(with WithActor) []*url.URL { + actorProp := with.GetActivityStreamsActor() + return getIRIs[vocab.ActivityStreamsActorPropertyIterator](actorProp) +} + +// AppendActor appends the given IRIs to the Actor property of 'with'. +func AppendActor(with WithActor, actor ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsActorPropertyIterator] { + actorProp := with.GetActivityStreamsActor() + if actorProp == nil { + actorProp = streams.NewActivityStreamsActorProperty() + with.SetActivityStreamsActor(actorProp) + } + return actorProp + }, actor...) +} + +// GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'. Panics on entries with missing ID. +func GetAttributedTo(with WithAttributedTo) []*url.URL { + attribProp := with.GetActivityStreamsAttributedTo() + return getIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp) +} + +// AppendAttributedTo appends the given IRIs to the AttributedTo property of 'with'. +func AppendAttributedTo(with WithAttributedTo, attribTo ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsAttributedToPropertyIterator] { + attribProp := with.GetActivityStreamsAttributedTo() + if attribProp == nil { + attribProp = streams.NewActivityStreamsAttributedToProperty() + with.SetActivityStreamsAttributedTo(attribProp) + } + return attribProp + }, attribTo...) +} + +// GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'. Panics on entries with missing ID. +func GetInReplyTo(with WithInReplyTo) []*url.URL { + replyProp := with.GetActivityStreamsInReplyTo() + return getIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp) +} + +// AppendInReplyTo appends the given IRIs to the InReplyTo property of 'with'. +func AppendInReplyTo(with WithInReplyTo, replyTo ...*url.URL) { + appendIRIs(func() Property[vocab.ActivityStreamsInReplyToPropertyIterator] { + replyProp := with.GetActivityStreamsInReplyTo() + if replyProp == nil { + replyProp = streams.NewActivityStreamsInReplyToProperty() + with.SetActivityStreamsInReplyTo(replyProp) + } + return replyProp + }, replyTo...) +} + +// GetPublished returns the time contained in the Published property of 'with'. +func GetPublished(with WithPublished) time.Time { + publishProp := with.GetActivityStreamsPublished() + if publishProp == nil { + return time.Time{} + } + return publishProp.Get() +} + +// SetPublished sets the given time on the Published property of 'with'. +func SetPublished(with WithPublished, published time.Time) { + publishProp := with.GetActivityStreamsPublished() + if publishProp == nil { + publishProp = streams.NewActivityStreamsPublishedProperty() + with.SetActivityStreamsPublished(publishProp) + } + publishProp.Set(published) +} + +// GetEndTime returns the time contained in the EndTime property of 'with'. +func GetEndTime(with WithEndTime) time.Time { + endTimeProp := with.GetActivityStreamsEndTime() + if endTimeProp == nil { + return time.Time{} + } + return endTimeProp.Get() +} + +// SetEndTime sets the given time on the EndTime property of 'with'. +func SetEndTime(with WithEndTime, end time.Time) { + endTimeProp := with.GetActivityStreamsEndTime() + if endTimeProp == nil { + endTimeProp = streams.NewActivityStreamsEndTimeProperty() + with.SetActivityStreamsEndTime(endTimeProp) + } + endTimeProp.Set(end) +} + +// GetEndTime returns the times contained in the Closed property of 'with'. +func GetClosed(with WithClosed) []time.Time { + closedProp := with.GetActivityStreamsClosed() + if closedProp == nil || closedProp.Len() == 0 { + return nil + } + closed := make([]time.Time, 0, closedProp.Len()) + for i := 0; i < closedProp.Len(); i++ { + at := closedProp.At(i) + if t := at.GetXMLSchemaDateTime(); !t.IsZero() { + closed = append(closed, t) + } + } + return closed +} + +// AppendClosed appends the given times to the Closed property of 'with'. +func AppendClosed(with WithClosed, closed ...time.Time) { + if len(closed) == 0 { + return + } + closedProp := with.GetActivityStreamsClosed() + if closedProp == nil { + closedProp = streams.NewActivityStreamsClosedProperty() + with.SetActivityStreamsClosed(closedProp) + } + for _, closed := range closed { + closedProp.AppendXMLSchemaDateTime(closed) + } +} + +// GetVotersCount returns the integer contained in the VotersCount property of 'with', if found. +func GetVotersCount(with WithVotersCount) int { + votersProp := with.GetTootVotersCount() + if votersProp == nil { + return 0 + } + return votersProp.Get() +} + +// SetVotersCount sets the given count on the VotersCount property of 'with'. +func SetVotersCount(with WithVotersCount, count int) { + votersProp := with.GetTootVotersCount() + if votersProp == nil { + votersProp = streams.NewTootVotersCountProperty() + with.SetTootVotersCount(votersProp) + } + votersProp.Set(count) +} + +func getIRIs[T TypeOrIRI](prop Property[T]) []*url.URL { + if prop == nil || prop.Len() == 0 { + return nil + } + ids := make([]*url.URL, 0, prop.Len()) + for i := 0; i < prop.Len(); i++ { + at := prop.At(i) + if t := at.GetType(); t != nil { + id := GetJSONLDId(t) + if id != nil { + ids = append(ids, id) + continue + } + } + if at.IsIRI() { + id := at.GetIRI() + if id != nil { + ids = append(ids, id) + continue + } + } + } + return ids +} + +func appendIRIs[T TypeOrIRI](getProp func() Property[T], iri ...*url.URL) { + if len(iri) == 0 { + return + } + prop := getProp() + if prop == nil { + // check outside loop. + panic("prop not set") + } + for _, iri := range iri { + prop.AppendIRI(iri) + } +} + +// panicfAt panics with a call to gtserror.NewfAt() with given args (+1 to calldepth). +// func panicfAt(calldepth int, msg string, args ...any) { +// panic(gtserror.NewfAt(calldepth+1, msg, args...)) +// } |