summaryrefslogtreecommitdiff
path: root/internal/ap
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ap')
-rw-r--r--internal/ap/extract.go65
-rw-r--r--internal/ap/extractfocus_test.go125
-rw-r--r--internal/ap/interfaces.go31
-rw-r--r--internal/ap/properties.go64
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.
//