diff options
Diffstat (limited to 'internal/ap')
| -rw-r--r-- | internal/ap/extract.go | 65 | ||||
| -rw-r--r-- | internal/ap/extractfocus_test.go | 125 | ||||
| -rw-r--r-- | internal/ap/interfaces.go | 31 | ||||
| -rw-r--r-- | internal/ap/properties.go | 64 |
4 files changed, 276 insertions, 9 deletions
diff --git a/internal/ap/extract.go b/internal/ap/extract.go index cc8129f04..596e29b13 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -634,32 +634,38 @@ func ExtractContent(i WithContent) gtsmodel.Content { return content } -// ExtractAttachments attempts to extract barebones MediaAttachment objects from given AS interface type. +// 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 + var ( + attachments = make([]*gtsmodel.MediaAttachment, 0, attachmentProp.Len()) + 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) + + attachmentable, ok := ToAttachmentable(t) if !ok { - errs.Appendf("incorrect attachment type: %T", t) + errs.Appendf("could not cast %T to Attachmentable", t) continue } + attachment, err := ExtractAttachment(attachmentable) if err != nil { errs.Appendf("error extracting attachment: %w", err) continue } + attachments = append(attachments, attachment) } @@ -681,7 +687,10 @@ func ExtractAttachment(i Attachmentable) (*gtsmodel.MediaAttachment, error) { RemoteURL: remoteURL.String(), Description: ExtractDescription(i), Blurhash: ExtractBlurhash(i), - Processing: gtsmodel.ProcessingStatusReceived, + FileMeta: gtsmodel.FileMeta{ + Focus: ExtractFocus(i), + }, + Processing: gtsmodel.ProcessingStatusReceived, }, nil } @@ -708,6 +717,50 @@ func ExtractBlurhash(i WithBlurhash) string { return blurhashProp.Get() } +// ExtractFocus parses a gtsmodel.Focus from the given Attachmentable's +// `focalPoint` property, if Attachmentable can have `focalPoint`, and +// `focalPoint` is set to a valid pair of floats. Otherwise, returns a +// zero gtsmodel.Focus (ie., focus in the centre of the image). +func ExtractFocus(attachmentable Attachmentable) gtsmodel.Focus { + focus := gtsmodel.Focus{} + + withFocalPoint, ok := attachmentable.(WithFocalPoint) + if !ok { + return focus + } + + focalPointProp := withFocalPoint.GetTootFocalPoint() + if focalPointProp == nil || focalPointProp.Len() != 2 { + return focus + } + + xProp := focalPointProp.At(0) + if !xProp.IsXMLSchemaFloat() { + return focus + } + + yProp := focalPointProp.At(1) + if !yProp.IsXMLSchemaFloat() { + return focus + } + + x := xProp.Get() + if x < -1 || x > 1 { + return focus + } + + y := yProp.Get() + if y < -1 || y > 1 { + return focus + } + + // Looks good. + focus.X = float32(x) + focus.Y = float32(y) + + return focus +} + // ExtractHashtags extracts a slice of minimal gtsmodel.Tags // from a WithTag. If an entry in the WithTag is not a hashtag, // or has a name that cannot be normalized, it will be ignored. diff --git a/internal/ap/extractfocus_test.go b/internal/ap/extractfocus_test.go new file mode 100644 index 000000000..9e7935740 --- /dev/null +++ b/internal/ap/extractfocus_test.go @@ -0,0 +1,125 @@ +// 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_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + + "code.superseriousbusiness.org/activity/streams" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/ap" +) + +type ExtractFocusTestSuite struct { + APTestSuite +} + +func (suite *ExtractFocusTestSuite) TestExtractFocus() { + ctx := context.Background() + + type test struct { + data string + expectX float32 + expectY float32 + } + + for _, test := range []test{ + { + // Fine. + data: "-0.5, 0.5", + expectX: -0.5, + expectY: 0.5, + }, + { + // Also fine. + data: "1, 1", + expectX: 1, + expectY: 1, + }, + { + // Out of range. + data: "1.5, 1", + expectX: 0, + expectY: 0, + }, + { + // Too many points. + data: "1, 1, 0", + expectX: 0, + expectY: 0, + }, + { + // Not enough points. + data: "1", + expectX: 0, + expectY: 0, + }, + } { + // Wrap provided test.data + // in a minimal Attachmentable. + const fmts = `{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "toot": "http://joinmastodon.org/ns#" + } + ], + "focalPoint": [ %s ], + "type": "Image" +}` + + // Unmarshal test data. + data := fmt.Sprintf(fmts, test.data) + m := make(map[string]any) + if err := json.Unmarshal([]byte(data), &m); err != nil { + suite.FailNow(err.Error()) + } + + // Convert to type. + t, err := streams.ToType(ctx, m) + if err != nil { + suite.FailNow(err.Error()) + } + + // Convert to attachmentable. + attachmentable, ok := t.(ap.Attachmentable) + if !ok { + suite.FailNow("", "%T was not Attachmentable", t) + } + + // Check extracted focus. + focus := ap.ExtractFocus(attachmentable) + if focus.X != test.expectX || focus.Y != test.expectY { + suite.Fail("", + "expected x=%.2f y=%.2f got x=%.2f y=%.2f", + test.expectX, test.expectY, focus.X, focus.Y, + ) + } + } +} + +func TestExtractFocusTestSuite(t *testing.T) { + suite.Run(t, new(ExtractFocusTestSuite)) +} diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index 1dcc6afef..28b5c0d20 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -165,6 +165,29 @@ func ToApprovable(t vocab.Type) (Approvable, bool) { return approvable, true } +// IsAttachmentable returns whether AS vocab type name +// is something that can be cast to Attachmentable. +func IsAttachmentable(typeName string) bool { + switch typeName { + case ObjectAudio, + ObjectDocument, + ObjectImage, + ObjectVideo: + return true + default: + return false + } +} + +// ToAttachmentable safely tries to cast vocab.Type as Attachmentable. +func ToAttachmentable(t vocab.Type) (Attachmentable, bool) { + attachmentable, ok := t.(Attachmentable) + if !ok || !IsAttachmentable(t.GetTypeName()) { + return nil, false + } + return attachmentable, 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). @@ -628,9 +651,11 @@ type WithBlurhash interface { SetTootBlurhash(vocab.TootBlurhashProperty) } -// type withFocalPoint interface { -// // TODO -// } +// WithFocalPoint represents an object with TootFocalPointProperty. +type WithFocalPoint interface { + GetTootFocalPoint() vocab.TootFocalPointProperty + SetTootFocalPoint(vocab.TootFocalPointProperty) +} // WithHref represents an activity with ActivityStreamsHrefProperty type WithHref interface { diff --git a/internal/ap/properties.go b/internal/ap/properties.go index ea925457a..589639337 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -560,6 +560,70 @@ func SetApprovedBy(with WithApprovedBy, approvedBy *url.URL) { abProp.Set(approvedBy) } +// GetMediaType returns the string contained in +// the MediaType property of 'with', if set. +func GetMediaType(with WithMediaType) string { + mtProp := with.GetActivityStreamsMediaType() + if mtProp == nil || !mtProp.IsRFCRfc2045() { + return "" + } + return mtProp.Get() +} + +// SetMediaType sets the given string +// on the MediaType property of 'with'. +func SetMediaType(with WithMediaType, mediaType string) { + mtProp := with.GetActivityStreamsMediaType() + if mtProp == nil { + mtProp = streams.NewActivityStreamsMediaTypeProperty() + with.SetActivityStreamsMediaType(mtProp) + } + mtProp.Set(mediaType) +} + +// AppendName appends the given name +// vals to the Name property of 'with'. +func AppendName(with WithName, name ...string) { + if len(name) == 0 { + return + } + nameProp := with.GetActivityStreamsName() + if nameProp == nil { + nameProp = streams.NewActivityStreamsNameProperty() + with.SetActivityStreamsName(nameProp) + } + for _, name := range name { + nameProp.AppendXMLSchemaString(name) + } +} + +// AppendSummary appends the given summary +// vals to the Summary property of 'with'. +func AppendSummary(with WithSummary, summary ...string) { + if len(summary) == 0 { + return + } + summaryProp := with.GetActivityStreamsSummary() + if summaryProp == nil { + summaryProp = streams.NewActivityStreamsSummaryProperty() + with.SetActivityStreamsSummary(summaryProp) + } + for _, summary := range summary { + summaryProp.AppendXMLSchemaString(summary) + } +} + +// SetBlurhash sets the given string +// on the Blurhash property of 'with'. +func SetBlurhash(with WithBlurhash, mediaType string) { + bProp := with.GetTootBlurhash() + if bProp == nil { + bProp = streams.NewTootBlurhashProperty() + with.SetTootBlurhash(bProp) + } + bProp.Set(mediaType) +} + // extractIRIs extracts just the AP IRIs from an iterable // property that may contain types (with IRIs) or just IRIs. // |
