From eb170003b81504ba6eb85f950c223dc9eaf1cfca Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:22:34 +0000 Subject: [PATCH] [bugfix] return 400 Bad Request on more cases of malformed AS data (#2399) --- go.mod | 6 +- go.sum | 12 +- internal/ap/extract.go | 23 - internal/ap/interfaces.go | 6 + internal/ap/properties.go | 195 ++++- internal/ap/resolve_test.go | 2 +- .../api/activitypub/users/inboxpost_test.go | 40 + internal/cache/util.go | 2 +- internal/email/common.go | 2 +- .../federation/dereferencing/account_test.go | 6 +- internal/federation/dereferencing/thread.go | 4 +- internal/federation/federatingactor.go | 19 +- internal/federation/federatingdb/util.go | 2 +- internal/gtserror/error.go | 59 +- internal/gtserror/new_caller.go | 2 +- internal/gtserror/withcode.go | 5 +- internal/httpclient/client.go | 2 +- internal/log/caller.go | 2 +- internal/log/format.go | 29 + internal/media/processingemoji.go | 2 +- internal/media/processingmedia.go | 2 +- internal/middleware/logger.go | 27 +- internal/processing/admin/email.go | 2 +- internal/processing/search/get.go | 6 +- internal/processing/search/lookup.go | 2 +- internal/typeutils/astointernal.go | 775 +++++++++--------- internal/typeutils/astointernal_test.go | 4 +- internal/typeutils/internaltoas.go | 2 +- internal/typeutils/util.go | 18 - internal/typeutils/wrap.go | 2 +- .../gruf/go-cache/v3/result/cache.go | 280 +++---- .../gruf/go-cache/v3/result/key.go | 23 +- .../codeberg.org/gruf/go-errors/v2/README.md | 9 +- .../gruf/go-errors/v2/build_caller.go | 26 + .../gruf/go-errors/v2/build_nocaller.go | 18 + .../gruf/go-errors/v2/build_notrace.go | 20 + .../gruf/go-errors/v2/build_trace.go | 27 + .../codeberg.org/gruf/go-errors/v2/callers.go | 98 --- .../codeberg.org/gruf/go-errors/v2/error.go | 35 - .../gruf/go-errors/v2/error_notrace.go | 33 - .../codeberg.org/gruf/go-errors/v2/errors.go | 231 +++++- vendor/codeberg.org/gruf/go-errors/v2/once.go | 36 +- .../codeberg.org/gruf/go-errors/v2/runtime.go | 97 +++ .../gruf/go-errors/v2/standard.go | 219 +++-- .../codeberg.org/gruf/go-errors/v2/value.go | 52 +- vendor/codeberg.org/gruf/go-runners/pool.go | 36 +- vendor/modules.txt | 6 +- 47 files changed, 1493 insertions(+), 1013 deletions(-) create mode 100644 internal/log/format.go create mode 100644 vendor/codeberg.org/gruf/go-errors/v2/build_caller.go create mode 100644 vendor/codeberg.org/gruf/go-errors/v2/build_nocaller.go create mode 100644 vendor/codeberg.org/gruf/go-errors/v2/build_notrace.go create mode 100644 vendor/codeberg.org/gruf/go-errors/v2/build_trace.go delete mode 100644 vendor/codeberg.org/gruf/go-errors/v2/callers.go delete mode 100644 vendor/codeberg.org/gruf/go-errors/v2/error.go delete mode 100644 vendor/codeberg.org/gruf/go-errors/v2/error_notrace.go create mode 100644 vendor/codeberg.org/gruf/go-errors/v2/runtime.go diff --git a/go.mod b/go.mod index e16507254..b7cd1a8f5 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,15 @@ toolchain go1.21.3 require ( codeberg.org/gruf/go-bytesize v1.0.2 codeberg.org/gruf/go-byteutil v1.2.0 - codeberg.org/gruf/go-cache/v3 v3.5.6 + codeberg.org/gruf/go-cache/v3 v3.5.7 codeberg.org/gruf/go-debug v1.3.0 - codeberg.org/gruf/go-errors/v2 v2.2.0 + codeberg.org/gruf/go-errors/v2 v2.3.1 codeberg.org/gruf/go-fastcopy v1.1.2 codeberg.org/gruf/go-iotools v0.0.0-20230811115124-5d4223615a7f codeberg.org/gruf/go-kv v1.6.4 codeberg.org/gruf/go-logger/v2 v2.2.1 codeberg.org/gruf/go-mutexes v1.3.1 - codeberg.org/gruf/go-runners v1.6.1 + codeberg.org/gruf/go-runners v1.6.2 codeberg.org/gruf/go-sched v1.2.3 codeberg.org/gruf/go-store/v2 v2.2.4 github.com/DmitriyVTitov/size v1.5.0 diff --git a/go.sum b/go.sum index a6468fa49..b300003c7 100644 --- a/go.sum +++ b/go.sum @@ -46,13 +46,13 @@ codeberg.org/gruf/go-bytesize v1.0.2 h1:Mo+ITi+0uZ4YNSZf2ed6Qw8acOI39W4mmgE1a8ls codeberg.org/gruf/go-bytesize v1.0.2/go.mod h1:n/GU8HzL9f3UNp/mUKyr1qVmTlj7+xacpp0OHfkvLPs= codeberg.org/gruf/go-byteutil v1.2.0 h1:YoxkpUOoHS82BcPXfiIcWLe/YhS8QhpNUHdfuhN09QM= codeberg.org/gruf/go-byteutil v1.2.0/go.mod h1:cWM3tgMCroSzqoBXUXMhvxTxYJp+TbCr6ioISRY5vSU= -codeberg.org/gruf/go-cache/v3 v3.5.6 h1:TJnNOuij5DF/ZK9pDB61SlYzxidRQeYjYYW3dfFSznc= -codeberg.org/gruf/go-cache/v3 v3.5.6/go.mod h1:NbsGQUgEdNFd631WSasvCHIVAaY9ovuiSeoBwtsIeDc= +codeberg.org/gruf/go-cache/v3 v3.5.7 h1:5hut49a8Wp3hdwrCEJYj6pHY2aRR1hyTmkK4+wHVYq4= +codeberg.org/gruf/go-cache/v3 v3.5.7/go.mod h1:Thahfuf3PgHSv2+1zHpvhRdX97tx1WXurVNGWpZucAM= codeberg.org/gruf/go-debug v1.3.0 h1:PIRxQiWUFKtGOGZFdZ3Y0pqyfI0Xr87j224IYe2snZs= codeberg.org/gruf/go-debug v1.3.0/go.mod h1:N+vSy9uJBQgpQcJUqjctvqFz7tBHJf+S/PIjLILzpLg= codeberg.org/gruf/go-errors/v2 v2.0.0/go.mod h1:ZRhbdhvgoUA3Yw6e56kd9Ox984RrvbEFC2pOXyHDJP4= -codeberg.org/gruf/go-errors/v2 v2.2.0 h1:CxnTtR4+BqRGeBHuG/FdCKM4m3otMdfPVez6ReBebkM= -codeberg.org/gruf/go-errors/v2 v2.2.0/go.mod h1:LfzD9nkAAJpEDbkUqOZQ2jdaQ8VrK0pnR36zLOMFq6Y= +codeberg.org/gruf/go-errors/v2 v2.3.1 h1:5+OChx06R8HT+OFB3KFetPdaptQYBS9XVZKKf30wIbk= +codeberg.org/gruf/go-errors/v2 v2.3.1/go.mod h1:LfzD9nkAAJpEDbkUqOZQ2jdaQ8VrK0pnR36zLOMFq6Y= codeberg.org/gruf/go-fastcopy v1.1.2 h1:YwmYXPsyOcRBxKEE2+w1bGAZfclHVaPijFsOVOcnNcw= codeberg.org/gruf/go-fastcopy v1.1.2/go.mod h1:GDDYR0Cnb3U/AIfGM3983V/L+GN+vuwVMvrmVABo21s= codeberg.org/gruf/go-fastpath/v2 v2.0.0 h1:iAS9GZahFhyWEH0KLhFEJR+txx1ZhMXxYzu2q5Qo9c0= @@ -69,8 +69,8 @@ codeberg.org/gruf/go-maps v1.0.3 h1:VDwhnnaVNUIy5O93CvkcE2IZXnMB1+IJjzfop9V12es= codeberg.org/gruf/go-maps v1.0.3/go.mod h1:D5LNDxlC9rsDuVQVM6JObaVGAdHB6g2dTdOdkh1aXWA= codeberg.org/gruf/go-mutexes v1.3.1 h1:8ibAjWwx08GJSq5R+lM9nwtJw2aAhMPKSXbfJ9EpDsA= codeberg.org/gruf/go-mutexes v1.3.1/go.mod h1:1j/6/MBeBQUedAtAtysLLnBKogfOZAxdym0E3wlaBD8= -codeberg.org/gruf/go-runners v1.6.1 h1:0KNiEfGnmNUs9intqxEAWqIKUyxVOmYTtn3kPVOHsjQ= -codeberg.org/gruf/go-runners v1.6.1/go.mod h1:QRcSExqXX8DM0rm8Xs6qX7baOzyvw0JIe4mu3TsQT+Y= +codeberg.org/gruf/go-runners v1.6.2 h1:oQef9niahfHu/wch14xNxlRMP8i+ABXH1Cb9PzZ4oYo= +codeberg.org/gruf/go-runners v1.6.2/go.mod h1:Tq5PrZ/m/rBXbLZz0u5if+yP3nG5Sf6S8O/GnyEePeQ= codeberg.org/gruf/go-sched v1.2.3 h1:H5ViDxxzOBR3uIyGBCf0eH8b1L8wMybOXcdtUUTXZHk= codeberg.org/gruf/go-sched v1.2.3/go.mod h1:vT9uB6KWFIIwnG9vcPY2a0alYNoqdL1mSzRM8I+PK7A= codeberg.org/gruf/go-store/v2 v2.2.4 h1:8HO1Jh2gg7boQKA3hsDAIXd9zwieu5uXwDXEcTOD9js= diff --git a/internal/ap/extract.go b/internal/ap/extract.go index 3d92fa2ba..987ff5e55 100644 --- a/internal/ap/extract.go +++ b/internal/ap/extract.go @@ -328,29 +328,6 @@ func ExtractAttributedToURI(i WithAttributedTo) (*url.URL, error) { return nil, gtserror.New("couldn't find iri for attributed to") } -// ExtractPublished extracts the published time from the given -// WithPublished. Will return an error if the published property -// is not set, is not a time.Time, or is zero. -func ExtractPublished(i WithPublished) (time.Time, error) { - t := time.Time{} - - publishedProp := i.GetActivityStreamsPublished() - if publishedProp == nil { - return t, gtserror.New("published prop was nil") - } - - if !publishedProp.IsXMLSchemaDateTime() { - return t, gtserror.New("published prop was not date time") - } - - t = publishedProp.Get() - if t.IsZero() { - return t, gtserror.New("published time was zero") - } - - return t, nil -} - // ExtractIconURI extracts the first URI it can find from // the given WithIcon which links to a supported image file. // Input will look something like this: diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index fed69d69d..f548dff0b 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -416,6 +416,12 @@ type WithOutbox interface { SetActivityStreamsOutbox(vocab.ActivityStreamsOutboxProperty) } +// WithSharedInbox represents an activity with ActivityStreamsSharedInboxProperty +type WithSharedInbox interface { + GetActivityStreamsSharedInbox() vocab.ActivityStreamsSharedInboxProperty + SetActivityStreamsSharedInbox(vocab.ActivityStreamsSharedInboxProperty) +} + // WithFollowing represents an activity with ActivityStreamsFollowingProperty type WithFollowing interface { GetActivityStreamsFollowing() vocab.ActivityStreamsFollowingProperty diff --git a/internal/ap/properties.go b/internal/ap/properties.go index 788b8ac4e..2b23c7cb2 100644 --- a/internal/ap/properties.go +++ b/internal/ap/properties.go @@ -55,14 +55,10 @@ func MustSet[W, T any](fn func(W, T) error, with W, value T) { // GetJSONLDId returns the ID of 'with', or nil. func GetJSONLDId(with WithJSONLDId) *url.URL { idProp := with.GetJSONLDId() - if idProp == nil { + if idProp == nil || !idProp.IsXMLSchemaAnyURI() { return nil } - id := idProp.Get() - if id == nil { - return nil - } - return id + return idProp.Get() } // SetJSONLDId sets the given URL to the JSONLD ID of 'with'. @@ -70,9 +66,9 @@ func SetJSONLDId(with WithJSONLDId, id *url.URL) { idProp := with.GetJSONLDId() if idProp == nil { idProp = streams.NewJSONLDIdProperty() + with.SetJSONLDId(idProp) } idProp.SetIRI(id) - with.SetJSONLDId(idProp) } // SetJSONLDIdStr sets the given string to the JSONLDID of 'with'. Returns error @@ -139,14 +135,46 @@ func AppendBcc(with WithBcc, bcc ...*url.URL) { }, bcc...) } -// GetActor returns the IRIs contained in the Actor property of 'with'. Panics on entries with missing ID. -func GetActor(with WithActor) []*url.URL { +// GetURL returns the IRIs contained in the URL property of 'with'. +func GetURL(with WithURL) []*url.URL { + urlProp := with.GetActivityStreamsUrl() + if urlProp == nil || urlProp.Len() == 0 { + return nil + } + urls := make([]*url.URL, 0, urlProp.Len()) + for i := 0; i < urlProp.Len(); i++ { + at := urlProp.At(i) + if at.IsXMLSchemaAnyURI() { + u := at.GetXMLSchemaAnyURI() + urls = append(urls, u) + } + } + return urls +} + +// AppendURL appends the given URLs to the URL property of 'with'. +func AppendURL(with WithURL, url ...*url.URL) { + if len(url) == 0 { + return + } + urlProp := with.GetActivityStreamsUrl() + if urlProp == nil { + urlProp = streams.NewActivityStreamsUrlProperty() + with.SetActivityStreamsUrl(urlProp) + } + for _, u := range url { + urlProp.AppendXMLSchemaAnyURI(u) + } +} + +// GetActorIRIs returns the IRIs contained in the Actor property of 'with'. +func GetActorIRIs(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) { +// AppendActorIRIs appends the given IRIs to the Actor property of 'with'. +func AppendActorIRIs(with WithActor, actor ...*url.URL) { appendIRIs(func() Property[vocab.ActivityStreamsActorPropertyIterator] { actorProp := with.GetActivityStreamsActor() if actorProp == nil { @@ -157,7 +185,25 @@ func AppendActor(with WithActor, actor ...*url.URL) { }, actor...) } -// GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'. Panics on entries with missing ID. +// GetObjectIRIs returns the IRIs contained in the Object property of 'with'. +func GetObjectIRIs(with WithObject) []*url.URL { + objectProp := with.GetActivityStreamsObject() + return getIRIs[vocab.ActivityStreamsObjectPropertyIterator](objectProp) +} + +// AppendObjectIRIs appends the given IRIs to the Object property of 'with'. +func AppendObjectIRIs(with WithObject) { + appendIRIs(func() Property[vocab.ActivityStreamsObjectPropertyIterator] { + objectProp := with.GetActivityStreamsObject() + if objectProp == nil { + objectProp = streams.NewActivityStreamsObjectProperty() + with.SetActivityStreamsObject(objectProp) + } + return objectProp + }) +} + +// GetAttributedTo returns the IRIs contained in the AttributedTo property of 'with'. func GetAttributedTo(with WithAttributedTo) []*url.URL { attribProp := with.GetActivityStreamsAttributedTo() return getIRIs[vocab.ActivityStreamsAttributedToPropertyIterator](attribProp) @@ -175,7 +221,7 @@ func AppendAttributedTo(with WithAttributedTo, attribTo ...*url.URL) { }, attribTo...) } -// GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'. Panics on entries with missing ID. +// GetInReplyTo returns the IRIs contained in the InReplyTo property of 'with'. func GetInReplyTo(with WithInReplyTo) []*url.URL { replyProp := with.GetActivityStreamsInReplyTo() return getIRIs[vocab.ActivityStreamsInReplyToPropertyIterator](replyProp) @@ -193,10 +239,105 @@ func AppendInReplyTo(with WithInReplyTo, replyTo ...*url.URL) { }, replyTo...) } +// GetInbox returns the IRI contained in the Inbox property of 'with'. +func GetInbox(with WithInbox) *url.URL { + inboxProp := with.GetActivityStreamsInbox() + if inboxProp == nil || !inboxProp.IsIRI() { + return nil + } + return inboxProp.GetIRI() +} + +// SetInbox sets the given IRI on the Inbox property of 'with'. +func SetInbox(with WithInbox, inbox *url.URL) { + inboxProp := with.GetActivityStreamsInbox() + if inboxProp == nil { + inboxProp = streams.NewActivityStreamsInboxProperty() + with.SetActivityStreamsInbox(inboxProp) + } + inboxProp.SetIRI(inbox) +} + +// GetOutbox returns the IRI contained in the Outbox property of 'with'. +func GetOutbox(with WithOutbox) *url.URL { + outboxProp := with.GetActivityStreamsOutbox() + if outboxProp == nil || !outboxProp.IsIRI() { + return nil + } + return outboxProp.GetIRI() +} + +// SetOutbox sets the given IRI on the Outbox property of 'with'. +func SetOutbox(with WithOutbox, outbox *url.URL) { + outboxProp := with.GetActivityStreamsOutbox() + if outboxProp == nil { + outboxProp = streams.NewActivityStreamsOutboxProperty() + with.SetActivityStreamsOutbox(outboxProp) + } + outboxProp.SetIRI(outbox) +} + +// GetFollowers returns the IRI contained in the Following property of 'with'. +func GetFollowing(with WithFollowing) *url.URL { + followProp := with.GetActivityStreamsFollowing() + if followProp == nil || !followProp.IsIRI() { + return nil + } + return followProp.GetIRI() +} + +// SetFollowers sets the given IRI on the Following property of 'with'. +func SetFollowing(with WithFollowing, following *url.URL) { + followProp := with.GetActivityStreamsFollowing() + if followProp == nil { + followProp = streams.NewActivityStreamsFollowingProperty() + with.SetActivityStreamsFollowing(followProp) + } + followProp.SetIRI(following) +} + +// GetFollowers returns the IRI contained in the Followers property of 'with'. +func GetFollowers(with WithFollowers) *url.URL { + followProp := with.GetActivityStreamsFollowers() + if followProp == nil || !followProp.IsIRI() { + return nil + } + return followProp.GetIRI() +} + +// SetFollowers sets the given IRI on the Followers property of 'with'. +func SetFollowers(with WithFollowers, followers *url.URL) { + followProp := with.GetActivityStreamsFollowers() + if followProp == nil { + followProp = streams.NewActivityStreamsFollowersProperty() + with.SetActivityStreamsFollowers(followProp) + } + followProp.SetIRI(followers) +} + +// GetFeatured returns the IRI contained in the Featured property of 'with'. +func GetFeatured(with WithFeatured) *url.URL { + featuredProp := with.GetTootFeatured() + if featuredProp == nil || !featuredProp.IsIRI() { + return nil + } + return featuredProp.GetIRI() +} + +// SetFeatured sets the given IRI on the Featured property of 'with'. +func SetFeatured(with WithFeatured, featured *url.URL) { + featuredProp := with.GetTootFeatured() + if featuredProp == nil { + featuredProp = streams.NewTootFeaturedProperty() + with.SetTootFeatured(featuredProp) + } + featuredProp.SetIRI(featured) +} + // GetPublished returns the time contained in the Published property of 'with'. func GetPublished(with WithPublished) time.Time { publishProp := with.GetActivityStreamsPublished() - if publishProp == nil { + if publishProp == nil || !publishProp.IsXMLSchemaDateTime() { return time.Time{} } return publishProp.Get() @@ -215,7 +356,7 @@ func SetPublished(with WithPublished, published time.Time) { // GetEndTime returns the time contained in the EndTime property of 'with'. func GetEndTime(with WithEndTime) time.Time { endTimeProp := with.GetActivityStreamsEndTime() - if endTimeProp == nil { + if endTimeProp == nil || !endTimeProp.IsXMLSchemaDateTime() { return time.Time{} } return endTimeProp.Get() @@ -240,7 +381,8 @@ func GetClosed(with WithClosed) []time.Time { closed := make([]time.Time, 0, closedProp.Len()) for i := 0; i < closedProp.Len(); i++ { at := closedProp.At(i) - if t := at.GetXMLSchemaDateTime(); !t.IsZero() { + if at.IsXMLSchemaDateTime() { + t := at.GetXMLSchemaDateTime() closed = append(closed, t) } } @@ -265,7 +407,7 @@ func AppendClosed(with WithClosed, closed ...time.Time) { // GetVotersCount returns the integer contained in the VotersCount property of 'with', if found. func GetVotersCount(with WithVotersCount) int { votersProp := with.GetTootVotersCount() - if votersProp == nil { + if votersProp == nil || !votersProp.IsXMLSchemaNonNegativeInteger() { return 0 } return votersProp.Get() @@ -281,6 +423,25 @@ func SetVotersCount(with WithVotersCount, count int) { votersProp.Set(count) } +// GetDiscoverable returns the boolean contained in the Discoverable property of 'with'. +func GetDiscoverable(with WithDiscoverable) bool { + discoverProp := with.GetTootDiscoverable() + if discoverProp == nil || !discoverProp.IsXMLSchemaBoolean() { + return false + } + return discoverProp.Get() +} + +// SetDiscoverable sets the given boolean on the Discoverable property of 'with'. +func SetDiscoverable(with WithDiscoverable, discoverable bool) { + discoverProp := with.GetTootDiscoverable() + if discoverProp == nil { + discoverProp = streams.NewTootDiscoverableProperty() + with.SetTootDiscoverable(discoverProp) + } + discoverProp.Set(discoverable) +} + func getIRIs[T TypeOrIRI](prop Property[T]) []*url.URL { if prop == nil || prop.Len() == 0 { return nil diff --git a/internal/ap/resolve_test.go b/internal/ap/resolve_test.go index 5ec1c4234..0b2c8fb0c 100644 --- a/internal/ap/resolve_test.go +++ b/internal/ap/resolve_test.go @@ -42,7 +42,7 @@ func (suite *ResolveTestSuite) TestResolveDocumentAsAccountable() { b := []byte(suite.typeToJson(suite.document1)) accountable, err := ap.ResolveAccountable(context.Background(), b) - suite.True(gtserror.WrongType(err)) + suite.True(gtserror.IsWrongType(err)) suite.EqualError(err, "ResolveAccountable: cannot resolve vocab type *typedocument.ActivityStreamsDocument as accountable") suite.Nil(accountable) } diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go index 7660050df..cde807d8d 100644 --- a/internal/api/activitypub/users/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -491,6 +491,46 @@ func (suite *InboxPostTestSuite) TestPostEmptyCreate() { ) } +func (suite *InboxPostTestSuite) TestPostCreateMalformedBlock() { + var ( + blockingAcc = suite.testAccounts["remote_account_1"] + blockedAcc = suite.testAccounts["local_account_1"] + activityID = blockingAcc.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" + ) + + block := streams.NewActivityStreamsBlock() + + // set the actor property to the block-ing account's URI + actorProp := streams.NewActivityStreamsActorProperty() + actorIRI := testrig.URLMustParse(blockingAcc.URI) + actorProp.AppendIRI(actorIRI) + block.SetActivityStreamsActor(actorProp) + + // set the ID property to the blocks's URI + idProp := streams.NewJSONLDIdProperty() + idProp.Set(testrig.URLMustParse(activityID)) + block.SetJSONLDId(idProp) + + // set the object property with MISSING block-ed URI. + objectProp := streams.NewActivityStreamsObjectProperty() + block.SetActivityStreamsObject(objectProp) + + // set the TO property to the target account's IRI + toProp := streams.NewActivityStreamsToProperty() + toIRI := testrig.URLMustParse(blockedAcc.URI) + toProp.AppendIRI(toIRI) + block.SetActivityStreamsTo(toProp) + + suite.inboxPost( + block, + blockingAcc, + blockedAcc, + http.StatusBadRequest, + `{"error":"Bad Request: malformed incoming activity"}`, + suite.signatureCheck, + ) +} + func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() { var ( requestingAccount = suite.testAccounts["remote_account_1"] diff --git a/internal/cache/util.go b/internal/cache/util.go index f15922401..924869aad 100644 --- a/internal/cache/util.go +++ b/internal/cache/util.go @@ -34,7 +34,7 @@ // ignoreErrors is an error matching function used to signal which errors // the result caches should NOT hold onto. these amount to anything non-permanent. func ignoreErrors(err error) bool { - return !errorsv2.Comparable( + return !errorsv2.IsV2( err, // the only cacheable errs, diff --git a/internal/email/common.go b/internal/email/common.go index ff148975f..57c9ab6d0 100644 --- a/internal/email/common.go +++ b/internal/email/common.go @@ -43,7 +43,7 @@ func (s *sender) sendTemplate(template string, subject string, data any, toAddre } if err := smtp.SendMail(s.hostAddress, s.auth, s.from, toAddresses, msg); err != nil { - return gtserror.SetType(err, gtserror.TypeSMTP) + return gtserror.SetSMTP(err) } return nil diff --git a/internal/federation/dereferencing/account_test.go b/internal/federation/dereferencing/account_test.go index 3b6994f08..ef1eddb91 100644 --- a/internal/federation/dereferencing/account_test.go +++ b/internal/federation/dereferencing/account_test.go @@ -175,7 +175,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUsername() "thisaccountdoesnotexist", config.GetHost(), ) - suite.True(gtserror.Unretrievable(err)) + suite.True(gtserror.IsUnretrievable(err)) suite.EqualError(err, db.ErrNoEntries.Error()) suite.Nil(fetchedAccount) } @@ -189,7 +189,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUsernameDom "thisaccountdoesnotexist", "localhost:8080", ) - suite.True(gtserror.Unretrievable(err)) + suite.True(gtserror.IsUnretrievable(err)) suite.EqualError(err, db.ErrNoEntries.Error()) suite.Nil(fetchedAccount) } @@ -202,7 +202,7 @@ func (suite *AccountTestSuite) TestDereferenceLocalAccountWithUnknownUserURI() { fetchingAccount.Username, testrig.URLMustParse("http://localhost:8080/users/thisaccountdoesnotexist"), ) - suite.True(gtserror.Unretrievable(err)) + suite.True(gtserror.IsUnretrievable(err)) suite.EqualError(err, db.ErrNoEntries.Error()) suite.Nil(fetchedAccount) } diff --git a/internal/federation/dereferencing/thread.go b/internal/federation/dereferencing/thread.go index 0ad8f09e4..243479db7 100644 --- a/internal/federation/dereferencing/thread.go +++ b/internal/federation/dereferencing/thread.go @@ -229,7 +229,7 @@ func (d *Dereferencer) DereferenceStatusAncestors(ctx context.Context, username l.Warnf("orphaned status: http error dereferencing parent: %v)", err) return nil - case gtserror.Unretrievable(err): + case gtserror.IsUnretrievable(err): // Not retrievable for some other reason, so just // bail for now; we can try again later if necessary. l.Warnf("orphaned status: parent unretrievable: %v)", err) @@ -354,7 +354,7 @@ func() *frame { // - any http type error for a new status returns unretrievable _, statusable, _, err := d.getStatusByURI(ctx, username, itemIRI) if err != nil { - if !gtserror.Unretrievable(err) { + if !gtserror.IsUnretrievable(err) { l.Errorf("error dereferencing remote status %s: %v", itemIRI, err) } continue itemLoop diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go index 774fa30af..81f3c3281 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -200,13 +200,18 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr // // Post the activity to the Actor's inbox and trigger side effects . if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil { - // Special case: We know it is a bad request if the object or - // target properties needed to be populated, but weren't. + // Special case: We know it is a bad request if the object or target + // props needed to be populated, or we failed parsing activity details. // 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. - log.Warnf(ctx, "malformed incoming activity: %v", err) - const text = "malformed activity: missing Object and / or Target" + if errors.Is(err, pub.ErrObjectRequired) || + errors.Is(err, pub.ErrTargetRequired) || + gtserror.IsMalformed(err) { + + // Log malformed activities to help debug. + l = l.WithField("activity", activity) + l.Warnf("malformed incoming activity: %v", err) + + const text = "malformed incoming activity" return false, gtserror.NewErrorBadRequest(errors.New(text), text) } @@ -234,7 +239,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr // This check may be removed when the `Exists()` func // is updated, and/or federating callbacks are handled // properly. - if !errorsv2.Comparable( + if !errorsv2.IsV2( err, db.ErrAlreadyExists, db.ErrNoEntries, diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index dd7a2240e..f1c565aeb 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -113,7 +113,7 @@ func (f *federatingDB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL, // If an actor URI has been set, create a new ID // based on actor (i.e. followER not the followEE). - if uri := ap.GetActor(follow); len(uri) == 1 { + if uri := ap.GetActorIRIs(follow); len(uri) == 1 { if actorAccount, err := f.state.DB.GetAccountByURI(ctx, uri[0].String()); err == nil { newID, err := id.NewRandomULID() if err != nil { diff --git a/internal/gtserror/error.go b/internal/gtserror/error.go index 21d580c4e..8338d30a4 100644 --- a/internal/gtserror/error.go +++ b/internal/gtserror/error.go @@ -35,39 +35,36 @@ errorTypeKey unrtrvableKey wrongTypeKey - - // Types returnable from Type(...). - TypeSMTP ErrorType = "smtp" // smtp (mail) + smtpKey + malformedKey ) -// Unretrievable checks error for a stored "unretrievable" flag. -// -// Unretrievable indicates that a call to retrieve a resource +// IsUnretrievable indicates that a call to retrieve a resource // (account, status, attachment, etc) could not be fulfilled, // either because it was not found locally, or because some // prerequisite remote resource call failed, making it impossible // to return the item. -func Unretrievable(err error) bool { +func IsUnretrievable(err error) bool { _, ok := errors.Value(err, unrtrvableKey).(struct{}) return ok } // SetUnretrievable will wrap the given error to store an "unretrievable" -// flag, returning wrapped error. See "Unretrievable" for example use-cases. +// flag, returning wrapped error. See Unretrievable() for example use-cases. func SetUnretrievable(err error) error { return errors.WithValue(err, unrtrvableKey, struct{}{}) } -// WrongType checks error for a stored "wrong type" flag. Wrong type +// IsWrongType checks error for a stored "wrong type" flag. Wrong type // indicates that an ActivityPub URI returned a type we weren't expecting: // Statusable instead of Accountable, or vice versa, for example. -func WrongType(err error) bool { +func IsWrongType(err error) bool { _, ok := errors.Value(err, wrongTypeKey).(struct{}) return ok } // SetWrongType will wrap the given error to store a "wrong type" flag, -// returning wrapped error. See "WrongType" for example use-cases. +// returning wrapped error. See IsWrongType() for example use-cases. func SetWrongType(err error) error { return errors.WithValue(err, wrongTypeKey, struct{}{}) } @@ -86,29 +83,41 @@ func WithStatusCode(err error, code int) error { return errors.WithValue(err, statusCodeKey, code) } -// NotFound checks error for a stored "not found" flag. For example -// an error from an outgoing HTTP request due to DNS lookup. -func NotFound(err error) bool { +// IsNotFound checks error for a stored "not found" flag. For +// example an error from an outgoing HTTP request due to DNS lookup. +func IsNotFound(err error) bool { _, ok := errors.Value(err, notFoundKey).(struct{}) return ok } // SetNotFound will wrap the given error to store a "not found" flag, -// returning wrapped error. See NotFound() for example use-cases. +// returning wrapped error. See IsNotFound() for example use-cases. func SetNotFound(err error) error { return errors.WithValue(err, notFoundKey, struct{}{}) } -// Type checks error for a stored "type" value. For example -// an error from sending an email may set a value of "smtp" -// to indicate this was an SMTP error. -func Type(err error) ErrorType { - s, _ := errors.Value(err, errorTypeKey).(ErrorType) - return s +// IsSMTP checks error for a stored "smtp" flag. For +// example an error from outgoing SMTP email attempt. +func IsSMTP(err error) bool { + _, ok := errors.Value(err, smtpKey).(struct{}) + return ok } -// SetType will wrap the given error to store a "type" value, -// returning wrapped error. See Type() for example use-cases. -func SetType(err error, errType ErrorType) error { - return errors.WithValue(err, errorTypeKey, errType) +// SetSMTP will wrap the given error to store an "smtp" flag, +// returning wrapped error. See IsSMTP() for example use-cases. +func SetSMTP(err error) error { + return errors.WithValue(err, smtpKey, struct{}{}) +} + +// IsMalformed checks error for a stored "malformed" flag. For +// example an error from an incoming ActivityStreams type conversion. +func IsMalformed(err error) bool { + _, ok := errors.Value(err, malformedKey).(struct{}) + return ok +} + +// SetMalformed will wrap the given error to store a "malformed" flag, +// returning wrapped error. See IsMalformed() for example use-cases. +func SetMalformed(err error) error { + return errors.WithValue(err, malformedKey, struct{}{}) } diff --git a/internal/gtserror/new_caller.go b/internal/gtserror/new_caller.go index 46ae76d6a..f7e0c84f6 100644 --- a/internal/gtserror/new_caller.go +++ b/internal/gtserror/new_caller.go @@ -61,7 +61,7 @@ func newfAt(calldepth int, msgf string, args ...any) error { } } -// caller fetches the calling function name, skipping 'depth'. Results are cached per PC. +// caller fetches the calling function name, skipping 'depth'. func caller(depth int) string { var pcs [1]uintptr diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go index d17a4e42e..da489225c 100644 --- a/internal/gtserror/withcode.go +++ b/internal/gtserror/withcode.go @@ -39,13 +39,16 @@ type WithCode interface { // Unwrap returns the original error. // This should *NEVER* be returned to a client as it may contain sensitive information. Unwrap() error + // Error serializes the original internal error for debugging within the GoToSocial logs. // This should *NEVER* be returned to a client as it may contain sensitive information. Error() string + // Safe returns the API-safe version of the error for serialization towards a client. // There's not much point logging this internally because it won't contain much helpful information. Safe() string - // Code returns the status code for serving to a client. + + // Code returns the status code for serving to a client. Code() int } diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index fd70cca7e..7adea7459 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -294,7 +294,7 @@ func (c *Client) DoSigned(r *http.Request, sign SignFunc) (rsp *http.Response, e _ = rsp.Body.Close() rsp = nil - } else if errorsv2.Comparable(err, + } else if errorsv2.IsV2(err, context.DeadlineExceeded, context.Canceled, ErrBodyTooLarge, diff --git a/internal/log/caller.go b/internal/log/caller.go index b6c2416cd..75b8d82d9 100644 --- a/internal/log/caller.go +++ b/internal/log/caller.go @@ -22,7 +22,7 @@ "strings" ) -// Caller fetches the calling function name, skipping 'depth'. Results are cached per PC. +// Caller fetches the calling function name, skipping 'depth'. func Caller(depth int) string { var pcs [1]uintptr diff --git a/internal/log/format.go b/internal/log/format.go new file mode 100644 index 000000000..e7468ed21 --- /dev/null +++ b/internal/log/format.go @@ -0,0 +1,29 @@ +// 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 . + +package log + +import "codeberg.org/gruf/go-kv/format" + +// VarDump returns a serialized, useful log / error output of given variable. +func VarDump(a any) string { + buf := getBuf() + format.Appendf(buf, "{:v}", a) + s := string(buf.B) + putBuf(buf) + return s +} diff --git a/internal/media/processingemoji.go b/internal/media/processingemoji.go index 4c18d4aad..56113b27b 100644 --- a/internal/media/processingemoji.go +++ b/internal/media/processingemoji.go @@ -95,7 +95,7 @@ func (p *ProcessingEmoji) load(ctx context.Context) (*gtsmodel.Emoji, bool, erro defer func() { // This is only done when ctx NOT cancelled. - done = err == nil || !errors.Comparable(err, + done = err == nil || !errors.IsV2(err, context.Canceled, context.DeadlineExceeded, ) diff --git a/internal/media/processingmedia.go b/internal/media/processingmedia.go index 74745305c..dd021f3cf 100644 --- a/internal/media/processingmedia.go +++ b/internal/media/processingmedia.go @@ -115,7 +115,7 @@ func (p *ProcessingMedia) load(ctx context.Context) (*gtsmodel.MediaAttachment, defer func() { // This is only done when ctx NOT cancelled. - done = err == nil || !errorsv2.Comparable(err, + done = err == nil || !errorsv2.IsV2(err, context.Canceled, context.DeadlineExceeded, ) diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go index f9b81d58a..676f9e691 100644 --- a/internal/middleware/logger.go +++ b/internal/middleware/logger.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" + "runtime" "time" "codeberg.org/gruf/go-bytesize" @@ -56,7 +57,10 @@ func Logger(logClientIP bool) gin.HandlerFunc { _ = c.Error(err) // Dump a stacktrace to error log - callers := errors.GetCallers(3, 10) + pcs := make([]uintptr, 10) + n := runtime.Callers(3, pcs) + iter := runtime.CallersFrames(pcs[:n]) + callers := errors.Callers(gatherFrames(iter, n)) log.WithContext(c.Request.Context()). WithField("stacktrace", callers).Error(err) } @@ -80,8 +84,9 @@ func Logger(logClientIP bool) gin.HandlerFunc { } // Create log entry with fields - l := log.WithContext(c.Request.Context()). - WithFields(fields...) + l := log.New() + l = l.WithContext(c.Request.Context()) + l = l.WithFields(fields...) // Default is info lvl := level.INFO @@ -119,3 +124,19 @@ func Logger(logClientIP bool) gin.HandlerFunc { c.Next() } } + +// gatherFrames gathers runtime frames from a frame iterator. +func gatherFrames(iter *runtime.Frames, n int) []runtime.Frame { + if iter == nil { + return nil + } + frames := make([]runtime.Frame, 0, n) + for { + f, ok := iter.Next() + if !ok { + break + } + frames = append(frames, f) + } + return frames +} diff --git a/internal/processing/admin/email.go b/internal/processing/admin/email.go index 88396db76..fb78f1fcc 100644 --- a/internal/processing/admin/email.go +++ b/internal/processing/admin/email.go @@ -47,7 +47,7 @@ func (p *Processor) EmailTest(ctx context.Context, account *gtsmodel.Account, to } if err := p.emailSender.SendTestEmail(toAddress, testData); err != nil { - if errorType := gtserror.Type(err); errorType == gtserror.TypeSMTP { + if gtserror.IsSMTP(err) { // An error occurred during the SMTP part. // We should indicate this to the caller, as // it will likely help them debug the issue. diff --git a/internal/processing/search/get.go b/internal/processing/search/get.go index 4c09f05bb..6b2125d81 100644 --- a/internal/processing/search/get.go +++ b/internal/processing/search/get.go @@ -382,7 +382,7 @@ func (p *Processor) accountsByNamestring( if err != nil { // Check for semi-expected error types. // On one of these, we can continue. - if !gtserror.Unretrievable(err) && !gtserror.WrongType(err) { + if !gtserror.IsUnretrievable(err) && !gtserror.IsWrongType(err) { err = gtserror.Newf("error looking up @%s@%s as account: %w", username, domain, err) return gtserror.NewErrorInternalError(err) } @@ -491,7 +491,7 @@ func (p *Processor) byURI( if err != nil { // Check for semi-expected error types. // On one of these, we can continue. - if !gtserror.Unretrievable(err) && !gtserror.WrongType(err) { + if !gtserror.IsUnretrievable(err) && !gtserror.IsWrongType(err) { err = gtserror.Newf("error looking up %s as account: %w", uri, err) return gtserror.NewErrorInternalError(err) } @@ -509,7 +509,7 @@ func (p *Processor) byURI( if err != nil { // Check for semi-expected error types. // On one of these, we can continue. - if !gtserror.Unretrievable(err) && !gtserror.WrongType(err) { + if !gtserror.IsUnretrievable(err) && !gtserror.IsWrongType(err) { err = gtserror.Newf("error looking up %s as status: %w", uri, err) return gtserror.NewErrorInternalError(err) } diff --git a/internal/processing/search/lookup.go b/internal/processing/search/lookup.go index b6f1eef52..f5c131841 100644 --- a/internal/processing/search/lookup.go +++ b/internal/processing/search/lookup.go @@ -92,7 +92,7 @@ func (p *Processor) Lookup( false, // never resolve! ) if err != nil { - if gtserror.Unretrievable(err) { + if gtserror.IsUnretrievable(err) { // ErrNotRetrievable is fine, just wrap it in // a 404 to indicate we couldn't find anything. err := fmt.Errorf("%s not found", query) diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go index c7908ad24..ad42a687d 100644 --- a/internal/typeutils/astointernal.go +++ b/internal/typeutils/astointernal.go @@ -20,7 +20,6 @@ import ( "context" "errors" - "fmt" "net/url" "github.com/miekg/dns" @@ -38,201 +37,204 @@ // // If accountDomain is provided then this value will be used as the account's Domain, else the AP ID host. func (c *Converter) ASRepresentationToAccount(ctx context.Context, accountable ap.Accountable, accountDomain string) (*gtsmodel.Account, error) { - // first check if we actually already know this account - uriProp := accountable.GetJSONLDId() - if uriProp == nil || !uriProp.IsIRI() { - return nil, errors.New("no id property found on person, or id was not an iri") + var err error + + // Extract URI from accountable + uriObj := ap.GetJSONLDId(accountable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - uri := uriProp.GetIRI() - // we don't know the account, or we're being told to update it, so we need to generate it from the person -- at least we already have the URI! - acct := >smodel.Account{} - acct.URI = uri.String() + // Stringify uri obj. + uri := uriObj.String() - // Username aka preferredUsername - // We need this one so bail if it's not set. - username, err := ap.ExtractPreferredUsername(accountable) + // Create DB account with URI + var acct gtsmodel.Account + acct.URI = uri + + // Check whether account is a usable actor type. + switch acct.ActorType = accountable.GetTypeName(); acct.ActorType { + + // people, groups, and organizations aren't bots + case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization: + acct.Bot = util.Ptr(false) + + // apps and services are + case ap.ActorApplication, ap.ActorService: + acct.Bot = util.Ptr(true) + + // we don't know what this is! + default: + err := gtserror.Newf("unusable actor type for %s", uri) + return nil, gtserror.SetMalformed(err) + } + + // Extract preferredUsername, this is a *requirement*. + acct.Username, err = ap.ExtractPreferredUsername(accountable) if err != nil { - return nil, fmt.Errorf("couldn't extract username: %s", err) + err := gtserror.Newf("unusable username for %s", uri) + return nil, gtserror.SetMalformed(err) } - acct.Username = username - // Domain + // Extract a preferred name (display name), fallback to username. + if displayName := ap.ExtractName(accountable); displayName != "" { + acct.DisplayName = displayName + } else { + acct.DisplayName = acct.Username + } + + // Check for separaate account + // domain to the instance hostname. if accountDomain != "" { acct.Domain = accountDomain } else { - acct.Domain = uri.Host + acct.Domain = uriObj.Host } // avatar aka icon // if this one isn't extractable in a format we recognise we'll just skip it - if avatarURL, err := ap.ExtractIconURI(accountable); err == nil { + avatarURL, err := ap.ExtractIconURI(accountable) + if err == nil { acct.AvatarRemoteURL = avatarURL.String() } // header aka image // if this one isn't extractable in a format we recognise we'll just skip it - if headerURL, err := ap.ExtractImageURI(accountable); err == nil { + headerURL, err := ap.ExtractImageURI(accountable) + if err == nil { acct.HeaderRemoteURL = headerURL.String() } - // display name aka name - // we default to the username, but take the more nuanced name property if it exists - if displayName := ap.ExtractName(accountable); displayName != "" { - acct.DisplayName = displayName - } else { - acct.DisplayName = username - } - // account emojis (used in bio, display name, fields) - if emojis, err := ap.ExtractEmojis(accountable); err != nil { - log.Infof(nil, "error extracting account emojis: %s", err) - } else { - acct.Emojis = emojis + acct.Emojis, err = ap.ExtractEmojis(accountable) + if err != nil { + log.Warnf(ctx, "error(s) extracting account emojis for %s: %v", uri, err) } - // fields aka attachment array + // Extract account attachments (key-value fields). acct.Fields = ap.ExtractFields(accountable) - // note aka summary + // Extract account note (bio / summary). acct.Note = ap.ExtractSummary(accountable) - // check for bot and actor type - switch accountable.GetTypeName() { - case ap.ActorPerson, ap.ActorGroup, ap.ActorOrganization: - // people, groups, and organizations aren't bots - bot := false - acct.Bot = &bot - // apps and services are - case ap.ActorApplication, ap.ActorService: - bot := true - acct.Bot = &bot - default: - // we don't know what this is! - return nil, fmt.Errorf("type name %s not recognised or not convertible to ap.ActivityStreamsActor", accountable.GetTypeName()) - } - acct.ActorType = accountable.GetTypeName() + // Assume: + // - memorial (TODO) + // - sensitive (TODO) + // - hide collections (TODO) + acct.Memorial = util.Ptr(false) + acct.Sensitive = util.Ptr(false) + acct.HideCollections = util.Ptr(false) - // assume not memorial (todo) - memorial := false - acct.Memorial = &memorial - - // assume not sensitive (todo) - sensitive := false - acct.Sensitive = &sensitive - - // assume not hide collections (todo) - hideCollections := false - acct.HideCollections = &hideCollections - - // locked aka manuallyApprovesFollowers - locked := true - acct.Locked = &locked // assume locked by default + // Extract 'manuallyApprovesFollowers', (i.e. locked account) maf := accountable.GetActivityStreamsManuallyApprovesFollowers() - if maf != nil && maf.IsXMLSchemaBoolean() { - locked = maf.Get() + + switch { + case maf != nil && !maf.IsXMLSchemaBoolean(): + log.Warnf(ctx, "unusable manuallyApprovesFollowers for %s", uri) + fallthrough + + case maf == nil: + // None given, use default. + acct.Locked = util.Ptr(true) + + default: + // Valid bool provided. + locked := maf.Get() acct.Locked = &locked } - // discoverable - // default to false -- take custom value if it's set though - discoverable := false + // Extract account discoverability (default = false). + discoverable := ap.GetDiscoverable(accountable) acct.Discoverable = &discoverable - d, err := ap.ExtractDiscoverable(accountable) - if err == nil { - acct.Discoverable = &d - } - // assume not rss feed - enableRSS := false - acct.EnableRSS = &enableRSS + // Assume not an RSS feed. + acct.EnableRSS = util.Ptr(false) - // url property - url, err := ap.ExtractURL(accountable) - if err == nil { - // take the URL if we can find it - acct.URL = url.String() + // Extract the URL property. + urls := ap.GetURL(accountable) + if len(urls) == 0 { + // just use account uri string + acct.URL = uri } else { - // otherwise just take the account URI as the URL - acct.URL = uri.String() + // else use provided URL string + acct.URL = urls[0].String() } - // InboxURI - if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil { - acct.InboxURI = accountable.GetActivityStreamsInbox().GetIRI().String() + // Extract the inbox IRI property. + inboxIRI := ap.GetInbox(accountable) + if inboxIRI != nil { + acct.InboxURI = inboxIRI.String() } - // SharedInboxURI: - // only trust shared inbox if it has at least two domains, - // from the right, in common with the domain of the account + // Extract the outbox IRI property. + outboxIRI := ap.GetOutbox(accountable) + if outboxIRI != nil { + acct.OutboxURI = outboxIRI.String() + } + + // Extract a SharedInboxURI, but only trust if equal to / subdomain of account's domain. if sharedInboxURI := ap.ExtractSharedInbox(accountable); // nocollapse sharedInboxURI != nil && dns.CompareDomainName(acct.Domain, sharedInboxURI.Host) >= 2 { sharedInbox := sharedInboxURI.String() acct.SharedInboxURI = &sharedInbox } - // OutboxURI - if accountable.GetActivityStreamsOutbox() != nil && accountable.GetActivityStreamsOutbox().GetIRI() != nil { - acct.OutboxURI = accountable.GetActivityStreamsOutbox().GetIRI().String() + // Extract the following IRI property. + followingURI := ap.GetFollowing(accountable) + if followingURI != nil { + acct.FollowingURI = followingURI.String() } - // FollowingURI - if accountable.GetActivityStreamsFollowing() != nil && accountable.GetActivityStreamsFollowing().GetIRI() != nil { - acct.FollowingURI = accountable.GetActivityStreamsFollowing().GetIRI().String() + // Extract the following IRI property. + followersURI := ap.GetFollowers(accountable) + if followersURI != nil { + acct.FollowersURI = followersURI.String() } - // FollowersURI - if accountable.GetActivityStreamsFollowers() != nil && accountable.GetActivityStreamsFollowers().GetIRI() != nil { - acct.FollowersURI = accountable.GetActivityStreamsFollowers().GetIRI().String() - } - - // FeaturedURI aka pinned collection: - // Only trust featured URI if it has at least two domains, - // from the right, in common with the domain of the account - if featured := accountable.GetTootFeatured(); featured != nil && featured.IsIRI() { - if featuredURI := featured.GetIRI(); // nocollapse - featuredURI != nil && dns.CompareDomainName(acct.Domain, featuredURI.Host) >= 2 { - acct.FeaturedCollectionURI = featuredURI.String() - } + // Extract a FeaturedURI, but only trust if equal to / subdomain of account's domain. + if featuredURI := ap.GetFeatured(accountable); // nocollapse + featuredURI != nil && dns.CompareDomainName(acct.Domain, featuredURI.Host) >= 2 { + acct.FeaturedCollectionURI = featuredURI.String() } // TODO: FeaturedTagsURI // TODO: alsoKnownAs - // publicKey + // Extract account public key and verify ownership to account. pkey, pkeyURL, pkeyOwnerID, err := ap.ExtractPublicKey(accountable) if err != nil { - return nil, fmt.Errorf("couldn't get public key for person %s: %s", uri.String(), err) - } - - if pkeyOwnerID.String() != acct.URI { - return nil, fmt.Errorf("public key %s was owned by %s and not by %s", pkeyURL, pkeyOwnerID, acct.URI) + err := gtserror.Newf("error extracting public key for %s: %w", uri, err) + return nil, gtserror.SetMalformed(err) + } else if pkeyOwnerID.String() != acct.URI { + err := gtserror.Newf("public key not owned by account %s", uri) + return nil, gtserror.SetMalformed(err) } acct.PublicKey = pkey acct.PublicKeyURI = pkeyURL.String() - return acct, nil + return &acct, nil } // ASStatus converts a remote activitystreams 'status' representation into a gts model status. func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusable) (*gtsmodel.Status, error) { var err error - status := new(gtsmodel.Status) - - // status.URI - // - // ActivityPub ID/URI of this status. - idProp := statusable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, gtserror.New("no id property found, or id was not an iri") + // Extract URI from statusable + uriObj := ap.GetJSONLDId(statusable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - status.URI = idProp.GetIRI().String() - l := log.WithContext(ctx). - WithField("statusURI", status.URI) + // Stringify uri obj. + uri := uriObj.String() + + // Create DB status with URI + var status gtsmodel.Status + status.URI = uri // status.URL // @@ -259,7 +261,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // Media attachments for later dereferencing. status.Attachments, err = ap.ExtractAttachments(statusable) if err != nil { - l.Warnf("error(s) extracting attachments: %v", err) + log.Warnf(ctx, "error(s) extracting attachments for %s: %v", uri, err) } // status.Poll @@ -269,7 +271,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab if pollable, ok := ap.ToPollable(statusable); ok { status.Poll, err = ap.ExtractPoll(pollable) if err != nil { - l.Warnf("error(s) extracting poll: %v", err) + log.Warnf(ctx, "error(s) extracting poll for %s: %v", uri, err) } } @@ -277,7 +279,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // // Hashtags for later dereferencing. if hashtags, err := ap.ExtractHashtags(statusable); err != nil { - l.Warnf("error extracting hashtags: %v", err) + log.Warnf(ctx, "error extracting hashtags for %s: %v", uri, err) } else { status.Tags = hashtags } @@ -286,7 +288,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // // Custom emojis for later dereferencing. if emojis, err := ap.ExtractEmojis(statusable); err != nil { - l.Warnf("error extracting emojis: %v", err) + log.Warnf(ctx, "error extracting emojis for %s: %v", uri, err) } else { status.Emojis = emojis } @@ -295,7 +297,7 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // // Mentions of other accounts for later dereferencing. if mentions, err := ap.ExtractMentions(statusable); err != nil { - l.Warnf("error extracting mentions: %v", err) + log.Warnf(ctx, "error extracting mentions for %s: %v", uri, err) } else { status.Mentions = mentions } @@ -312,14 +314,13 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // status.Published // - // Publication time of this status. Thanks to - // db defaults, will fall back to now if not set. - published, err := ap.ExtractPublished(statusable) - if err != nil { - l.Warnf("error extracting published: %v", err) + // Extract published time for the boost, + // zero-time will fall back to db defaults. + if pub := ap.GetPublished(statusable); !pub.IsZero() { + status.CreatedAt = pub + status.UpdatedAt = pub } else { - status.CreatedAt = published - status.UpdatedAt = published + log.Warnf(ctx, "unusable published property on %s", uri) } // status.AccountURI @@ -329,20 +330,17 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // Account that created the status. Assume we have // this in the db by the time this function is called, // error if we don't. - attributedTo, err := ap.ExtractAttributedToURI(statusable) + status.Account, err = c.getASAttributedToAccount(ctx, + status.URI, + statusable, + ) if err != nil { - return nil, gtserror.Newf("error extracting attributed to uri: %w", err) - } - accountURI := attributedTo.String() - - account, err := c.state.DB.GetAccountByURI(ctx, accountURI) - if err != nil { - err = gtserror.Newf("db error getting status author account %s: %w", accountURI, err) return nil, err } - status.AccountURI = accountURI - status.AccountID = account.ID - status.Account = account + + // Set the related status<->account fields. + status.AccountURI = status.Account.URI + status.AccountID = status.Account.ID // status.InReplyToURI // status.InReplyToID @@ -353,15 +351,17 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab // Status that this status replies to, if applicable. // If we don't have this status in the database, we // just set the URI and assume we can deref it later. - if uri := ap.ExtractInReplyToURI(statusable); uri != nil { - inReplyToURI := uri.String() + inReplyTo := ap.GetInReplyTo(statusable) + if len(inReplyTo) > 0 { + + // Extract the URI from inReplyTo slice. + inReplyToURI := inReplyTo[0].String() status.InReplyToURI = inReplyToURI // Check if we already have the replied-to status. inReplyTo, err := c.state.DB.GetStatusByURI(ctx, inReplyToURI) if err != nil && !errors.Is(err, db.ErrNoEntries) { - // Real database error. - err = gtserror.Newf("db error getting replied-to status %s: %w", inReplyToURI, err) + err := gtserror.Newf("error getting reply %s from db: %w", inReplyToURI, err) return nil, err } @@ -375,16 +375,15 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab } } - // status.Visibility - visibility, err := ap.ExtractVisibility( + // Calculate intended visibility of the status. + status.Visibility, err = ap.ExtractVisibility( statusable, status.Account.FollowersURI, ) if err != nil { - err = gtserror.Newf("error extracting visibility: %w", err) - return nil, err + err := gtserror.Newf("error extracting status visibility for %s: %w", uri, err) + return nil, gtserror.SetMalformed(err) } - status.Visibility = visibility // Advanced visibility toggles for this status. // @@ -397,47 +396,40 @@ func (c *Converter) ASStatusToStatus(ctx context.Context, statusable ap.Statusab status.Likeable = util.Ptr(true) // status.Sensitive - status.Sensitive = func() *bool { - s := ap.ExtractSensitive(statusable) - return &s - }() + sensitive := ap.ExtractSensitive(statusable) + status.Sensitive = &sensitive // ActivityStreamsType status.ActivityStreamsType = statusable.GetTypeName() - return status, nil + return &status, nil } // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow request. func (c *Converter) ASFollowToFollowRequest(ctx context.Context, followable ap.Followable) (*gtsmodel.FollowRequest, error) { - idProp := followable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("no id property set on follow, or was not an iri") - } - uri := idProp.GetIRI().String() - - origin, err := ap.ExtractActorURI(followable) - if err != nil { - return nil, errors.New("error extracting actor property from follow") - } - originAccount, err := c.state.DB.GetAccountByURI(ctx, origin.String()) - if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + uriObj := ap.GetJSONLDId(followable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - target, err := ap.ExtractObjectURI(followable) + // Stringify uri obj. + uri := uriObj.String() + + origin, err := c.getASActorAccount(ctx, uri, followable) if err != nil { - return nil, errors.New("error extracting object property from follow") + return nil, err } - targetAccount, err := c.state.DB.GetAccountByURI(ctx, target.String()) + + target, err := c.getASObjectAccount(ctx, uri, followable) if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + return nil, err } followRequest := >smodel.FollowRequest{ URI: uri, - AccountID: originAccount.ID, - TargetAccountID: targetAccount.ID, + AccountID: origin.ID, + TargetAccountID: target.ID, } return followRequest, nil @@ -445,34 +437,29 @@ func (c *Converter) ASFollowToFollowRequest(ctx context.Context, followable ap.F // ASFollowToFollowRequest converts a remote activitystreams `follow` representation into gts model follow. func (c *Converter) ASFollowToFollow(ctx context.Context, followable ap.Followable) (*gtsmodel.Follow, error) { - idProp := followable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("no id property set on follow, or was not an iri") - } - uri := idProp.GetIRI().String() - - origin, err := ap.ExtractActorURI(followable) - if err != nil { - return nil, errors.New("error extracting actor property from follow") - } - originAccount, err := c.state.DB.GetAccountByURI(ctx, origin.String()) - if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + uriObj := ap.GetJSONLDId(followable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - target, err := ap.ExtractObjectURI(followable) + // Stringify uri obj. + uri := uriObj.String() + + origin, err := c.getASActorAccount(ctx, uri, followable) if err != nil { - return nil, errors.New("error extracting object property from follow") + return nil, err } - targetAccount, err := c.state.DB.GetAccountByURI(ctx, target.String()) + + target, err := c.getASObjectAccount(ctx, uri, followable) if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + return nil, err } follow := >smodel.Follow{ URI: uri, - AccountID: originAccount.ID, - TargetAccountID: targetAccount.ID, + AccountID: origin.ID, + TargetAccountID: target.ID, } return follow, nil @@ -480,85 +467,62 @@ func (c *Converter) ASFollowToFollow(ctx context.Context, followable ap.Followab // ASLikeToFave converts a remote activitystreams 'like' representation into a gts model status fave. func (c *Converter) ASLikeToFave(ctx context.Context, likeable ap.Likeable) (*gtsmodel.StatusFave, error) { - idProp := likeable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("no id property set on like, or was not an iri") - } - uri := idProp.GetIRI().String() - - origin, err := ap.ExtractActorURI(likeable) - if err != nil { - return nil, errors.New("error extracting actor property from like") - } - originAccount, err := c.state.DB.GetAccountByURI(ctx, origin.String()) - if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + uriObj := ap.GetJSONLDId(likeable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - target, err := ap.ExtractObjectURI(likeable) + // Stringify uri obj. + uri := uriObj.String() + + origin, err := c.getASActorAccount(ctx, uri, likeable) if err != nil { - return nil, errors.New("error extracting object property from like") + return nil, err } - targetStatus, err := c.state.DB.GetStatusByURI(ctx, target.String()) + target, err := c.getASObjectStatus(ctx, uri, likeable) if err != nil { - return nil, fmt.Errorf("error extracting status with uri %s from the database: %s", target.String(), err) - } - - var targetAccount *gtsmodel.Account - if targetStatus.Account != nil { - targetAccount = targetStatus.Account - } else { - a, err := c.state.DB.GetAccountByID(ctx, targetStatus.AccountID) - if err != nil { - return nil, fmt.Errorf("error extracting account with id %s from the database: %s", targetStatus.AccountID, err) - } - targetAccount = a + return nil, err } return >smodel.StatusFave{ - AccountID: originAccount.ID, - Account: originAccount, - TargetAccountID: targetAccount.ID, - TargetAccount: targetAccount, - StatusID: targetStatus.ID, - Status: targetStatus, + AccountID: origin.ID, + Account: origin, + TargetAccountID: target.AccountID, + TargetAccount: target.Account, + StatusID: target.ID, + Status: target, URI: uri, }, nil } // ASBlockToBlock converts a remote activity streams 'block' representation into a gts model block. func (c *Converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) (*gtsmodel.Block, error) { - idProp := blockable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("ASBlockToBlock: no id property set on block, or was not an iri") - } - uri := idProp.GetIRI().String() - - origin, err := ap.ExtractActorURI(blockable) - if err != nil { - return nil, errors.New("ASBlockToBlock: error extracting actor property from block") - } - originAccount, err := c.state.DB.GetAccountByURI(ctx, origin.String()) - if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + uriObj := ap.GetJSONLDId(blockable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - target, err := ap.ExtractObjectURI(blockable) + // Stringify uri obj. + uri := uriObj.String() + + origin, err := c.getASActorAccount(ctx, uri, blockable) if err != nil { - return nil, errors.New("ASBlockToBlock: error extracting object property from block") + return nil, err } - targetAccount, err := c.state.DB.GetAccountByURI(ctx, target.String()) + target, err := c.getASObjectAccount(ctx, uri, blockable) if err != nil { - return nil, fmt.Errorf("error extracting account with uri %s from the database: %s", origin.String(), err) + return nil, err } return >smodel.Block{ - AccountID: originAccount.ID, - Account: originAccount, - TargetAccountID: targetAccount.ID, - TargetAccount: targetAccount, + AccountID: origin.ID, + Account: origin, + TargetAccountID: target.ID, + TargetAccount: target, URI: uri, }, nil } @@ -586,23 +550,24 @@ func (c *Converter) ASBlockToBlock(ctx context.Context, blockable ap.Blockable) // seen before by this instance. If it was, then status.BoostOf should be a // fully filled-out status. If not, then only status.BoostOf.URI will be set. func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Announceable) (*gtsmodel.Status, bool, error) { - // Ensure item has an ID URI set. - _, statusURIStr, err := getURI(announceable) - if err != nil { - err = gtserror.Newf("error extracting URI: %w", err) - return nil, false, err + // Default assume + // we already have. + isNew := false + + // Extract uri ID from announceable. + uriObj := ap.GetJSONLDId(announceable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, isNew, gtserror.SetMalformed(err) } - var ( - status *gtsmodel.Status - isNew bool - ) + // Stringify uri obj. + uri := uriObj.String() // Check if we already have this boost in the database. - status, err = c.state.DB.GetStatusByURI(ctx, statusURIStr) + status, err := c.state.DB.GetStatusByURI(ctx, uri) if err != nil && !errors.Is(err, db.ErrNoEntries) { - // Real database error. - err = gtserror.Newf("db error trying to get status with uri %s: %w", statusURIStr, err) + err = gtserror.Newf("db error trying to get status with uri %s: %w", uri, err) return nil, isNew, err } @@ -612,66 +577,55 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno return status, isNew, nil } - // If we reach here, we're dealing - // with a boost we haven't seen before. + // Create DB status with URI + status = new(gtsmodel.Status) + status.URI = uri isNew = true - // Start assembling the new status - // (we already know the URI). - status = new(gtsmodel.Status) - status.URI = statusURIStr - // Get the URI of the boosted status. - boostOfURI, err := ap.ExtractObjectURI(announceable) - if err != nil { - err = gtserror.Newf("error extracting Object: %w", err) - return nil, isNew, err + boostOf := ap.GetObjectIRIs(announceable) + if len(boostOf) == 0 { + err := gtserror.Newf("unusable object property iri for %s", uri) + return nil, isNew, gtserror.SetMalformed(err) } // Set the URI of the boosted status on // the new status, for later dereferencing. - boostOf := >smodel.Status{ - URI: boostOfURI.String(), - } - status.BoostOf = boostOf + status.BoostOf = new(gtsmodel.Status) + status.BoostOf.URI = boostOf[0].String() - // Extract published time for the boost. - published, err := ap.ExtractPublished(announceable) + // Extract published time for the boost, + // zero-time will fall back to db defaults. + if pub := ap.GetPublished(announceable); !pub.IsZero() { + status.CreatedAt = pub + status.UpdatedAt = pub + } else { + log.Warnf(ctx, "unusable published property on %s", uri) + } + + // Extract and load the boost actor account, + // (this MUST already be in database by now). + status.Account, err = c.getASActorAccount(ctx, + uri, + announceable, + ) if err != nil { - err = gtserror.Newf("error extracting published: %w", err) return nil, isNew, err } - status.CreatedAt = published - status.UpdatedAt = published - // Extract URI of the boosting account. - accountURI, err := ap.ExtractActorURI(announceable) - if err != nil { - err = gtserror.Newf("error extracting Actor: %w", err) - return nil, isNew, err - } - accountURIStr := accountURI.String() - - // Try to get the boosting account based on the URI. - // This should have been dereferenced already before - // we hit this point so we can confidently error out - // if we don't have it. - account, err := c.state.DB.GetAccountByURI(ctx, accountURIStr) - if err != nil { - err = gtserror.Newf("db error trying to get account with uri %s: %w", accountURIStr, err) - return nil, isNew, err - } - status.AccountID = account.ID - status.AccountURI = account.URI - status.Account = account + // Set the related status<->account fields. + status.AccountURI = status.Account.URI + status.AccountID = status.Account.ID // Calculate intended visibility of the boost. - visibility, err := ap.ExtractVisibility(announceable, account.FollowersURI) + status.Visibility, err = ap.ExtractVisibility( + announceable, + status.Account.FollowersURI, + ) if err != nil { - err = gtserror.Newf("error extracting visibility: %w", err) - return nil, isNew, err + err := gtserror.Newf("error extracting status visibility for %s: %w", uri, err) + return nil, isNew, gtserror.SetMalformed(err) } - status.Visibility = visibility // Below IDs will all be included in the // boosted status, so set them empty here. @@ -688,32 +642,37 @@ func (c *Converter) ASAnnounceToStatus(ctx context.Context, announceable ap.Anno // ASFlagToReport converts a remote activitystreams 'flag' representation into a gts model report. func (c *Converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) (*gtsmodel.Report, error) { - // Extract flag uri. - idProp := flaggable.GetJSONLDId() - if idProp == nil || !idProp.IsIRI() { - return nil, errors.New("ASFlagToReport: no id property set on flaggable, or was not an iri") + uriObj := ap.GetJSONLDId(flaggable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - uri := idProp.GetIRI().String() - // Extract account that created the flag / report. - // This will usually be an instance actor. - actor, err := ap.ExtractActorURI(flaggable) + // Stringify uri obj. + uri := uriObj.String() + + // Extract the origin (actor) account for report. + origin, err := c.getASActorAccount(ctx, uri, flaggable) if err != nil { - return nil, fmt.Errorf("ASFlagToReport: error extracting actor: %w", err) - } - account, err := c.state.DB.GetAccountByURI(ctx, actor.String()) - if err != nil { - return nil, fmt.Errorf("ASFlagToReport: error in db fetching account with uri %s: %w", actor.String(), err) + return nil, err } + var ( + // Gathered from objects + // (+ content for misskey). + statusURIs []*url.URL + targetAccURI *url.URL + + // Get current hostname. + host = config.GetHost() + ) + // Get the content of the report. // For Mastodon, this will just be a string, or nothing. // In Misskey's case, it may also contain the URLs of // one or more reported statuses, so extract these too. content := ap.ExtractContent(flaggable).Content - statusURIs := []*url.URL{} - inlineURLs := misskeyReportInlineURLs(content) - statusURIs = append(statusURIs, inlineURLs...) + statusURIs = misskeyReportInlineURLs(content) // Extract account and statuses targeted by the flag / report. // @@ -725,86 +684,164 @@ func (c *Converter) ASFlagToReport(ctx context.Context, flaggable ap.Flaggable) // maybe some statuses. // // Throw away anything that's not relevant to us. - objects, err := ap.ExtractObjectURIs(flaggable) - if err != nil { - return nil, fmt.Errorf("ASFlagToReport: error extracting objects: %w", err) - } + objects := ap.GetObjectIRIs(flaggable) if len(objects) == 0 { - return nil, errors.New("ASFlagToReport: flaggable objects empty, can't create report") + err := gtserror.Newf("unusable object property iris for %s", uri) + return nil, gtserror.SetMalformed(err) } - var targetAccountURI *url.URL for _, object := range objects { switch { - case object.Host != config.GetHost(): - // object doesn't belong to us, just ignore it + case object.Host != host: + // object doesn't belong + // to us, just ignore it continue + case uris.IsUserPath(object): - if targetAccountURI != nil { - return nil, errors.New("ASFlagToReport: flaggable objects contained more than one target account uri") + if targetAccURI != nil { + err := gtserror.Newf("multiple target account uris for %s", uri) + return nil, gtserror.SetMalformed(err) } - targetAccountURI = object + targetAccURI = object + case uris.IsStatusesPath(object): statusURIs = append(statusURIs, object) } } - // Make sure we actually have a target account now. - if targetAccountURI == nil { - return nil, errors.New("ASFlagToReport: flaggable objects contained no recognizable target account uri") - } - targetAccount, err := c.state.DB.GetAccountByURI(ctx, targetAccountURI.String()) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("ASFlagToReport: account with uri %s could not be found in the db", targetAccountURI.String()) - } - return nil, fmt.Errorf("ASFlagToReport: db error getting account with uri %s: %w", targetAccountURI.String(), err) + // Ensure we have a target. + if targetAccURI == nil { + err := gtserror.Newf("missing target account uri for %s", uri) + return nil, gtserror.SetMalformed(err) + } + + // Fetch target account from the database by its URI. + targetAcc, err := c.state.DB.GetAccountByURI(ctx, targetAccURI.String()) + if err != nil { + return nil, gtserror.Newf("error getting target account %s from database: %w", targetAccURI, err) } - // If we got some status URIs, try to get them from the db now var ( + // Preallocate expected status + IDs slice lengths. statusIDs = make([]string, 0, len(statusURIs)) statuses = make([]*gtsmodel.Status, 0, len(statusURIs)) ) - for _, statusURI := range statusURIs { - statusURIString := statusURI.String() - // try getting this status by URI first, then URL - status, err := c.state.DB.GetStatusByURI(ctx, statusURIString) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("ASFlagToReport: db error getting status with uri %s: %w", statusURIString, err) + for _, statusURI := range statusURIs { + // Rescope as just the URI string. + statusURI := statusURI.String() + + // Try getting status by URI from database. + status, err := c.state.DB.GetStatusByURI(ctx, statusURI) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting target status %s from database: %w", statusURI, err) + return nil, err + } + + if status == nil { + // Status was not found, try again with URL. + status, err = c.state.DB.GetStatusByURL(ctx, statusURI) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := gtserror.Newf("error getting target status %s from database: %w", statusURI, err) + return nil, err } - status, err = c.state.DB.GetStatusByURL(ctx, statusURIString) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - return nil, fmt.Errorf("ASFlagToReport: db error getting status with url %s: %w", statusURIString, err) - } - - log.Warnf(nil, "reported status %s could not be found in the db, skipping it", statusURIString) + if status == nil { + log.Warnf(ctx, "missing target status %s for %s", statusURI, uri) continue } } - if status.AccountID != targetAccount.ID { - // status doesn't belong to this account, ignore it + if status.AccountID != targetAcc.ID { + // status doesn't belong + // to target, ignore it. continue } + // Append the discovered status to slices. statusIDs = append(statusIDs, status.ID) statuses = append(statuses, status) } - // id etc should be handled the caller, so just return what we got + // id etc should be handled the caller, + // so just return what we got return >smodel.Report{ URI: uri, - AccountID: account.ID, - Account: account, - TargetAccountID: targetAccount.ID, - TargetAccount: targetAccount, + AccountID: origin.ID, + Account: origin, + TargetAccountID: targetAcc.ID, + TargetAccount: targetAcc, Comment: content, StatusIDs: statusIDs, Statuses: statuses, }, nil } + +func (c *Converter) getASActorAccount(ctx context.Context, id string, with ap.WithActor) (*gtsmodel.Account, error) { + // Get actor IRIs from type. + actor := ap.GetActorIRIs(with) + if len(actor) == 0 { + err := gtserror.Newf("unusable actor property iri for %s", id) + return nil, gtserror.SetMalformed(err) + } + + // Check for account in database with provided actor URI. + account, err := c.state.DB.GetAccountByURI(ctx, actor[0].String()) + if err != nil { + return nil, gtserror.Newf("error getting actor account from database: %w", err) + } + + return account, nil +} + +func (c *Converter) getASAttributedToAccount(ctx context.Context, id string, with ap.WithAttributedTo) (*gtsmodel.Account, error) { + // Get attribTo IRIs from type. + attribTo := ap.GetAttributedTo(with) + if len(attribTo) == 0 { + err := gtserror.Newf("unusable attributedTo property iri for %s", id) + return nil, gtserror.SetMalformed(err) + } + + // Check for account in database with provided attributedTo URI. + account, err := c.state.DB.GetAccountByURI(ctx, attribTo[0].String()) + if err != nil { + return nil, gtserror.Newf("error getting actor account from database: %w", err) + } + + return account, nil + +} + +func (c *Converter) getASObjectAccount(ctx context.Context, id string, with ap.WithObject) (*gtsmodel.Account, error) { + // Get object IRIs from type. + object := ap.GetObjectIRIs(with) + if len(object) == 0 { + err := gtserror.Newf("unusable object property iri for %s", id) + return nil, gtserror.SetMalformed(err) + } + + // Check for account in database with provided object URI. + account, err := c.state.DB.GetAccountByURI(ctx, object[0].String()) + if err != nil { + return nil, gtserror.Newf("error getting object account from database: %w", err) + } + + return account, nil +} + +func (c *Converter) getASObjectStatus(ctx context.Context, id string, with ap.WithObject) (*gtsmodel.Status, error) { + // Get object IRIs from type. + object := ap.GetObjectIRIs(with) + if len(object) == 0 { + err := gtserror.Newf("unusable object property iri for %s", id) + return nil, gtserror.SetMalformed(err) + } + + // Check for status in database with provided object URI. + status, err := c.state.DB.GetStatusByURI(ctx, object[0].String()) + if err != nil { + return nil, gtserror.Newf("error getting object account from database: %w", err) + } + + return status, nil +} diff --git a/internal/typeutils/astointernal_test.go b/internal/typeutils/astointernal_test.go index 851d57efc..40bf4c855 100644 --- a/internal/typeutils/astointernal_test.go +++ b/internal/typeutils/astointernal_test.go @@ -363,7 +363,7 @@ func (suite *ASToInternalTestSuite) TestParseFlag3() { report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag) suite.Nil(report) - suite.EqualError(err, "ASFlagToReport: account with uri http://localhost:8080/users/mr_e_man could not be found in the db") + suite.EqualError(err, "ASFlagToReport: error getting target account http://localhost:8080/users/mr_e_man from database: sql: no rows in result set") } func (suite *ASToInternalTestSuite) TestParseFlag4() { @@ -388,7 +388,7 @@ func (suite *ASToInternalTestSuite) TestParseFlag4() { report, err := suite.typeconverter.ASFlagToReport(context.Background(), asFlag) suite.Nil(report) - suite.EqualError(err, "ASFlagToReport: flaggable objects contained no recognizable target account uri") + suite.EqualError(err, "ASFlagToReport: missing target account uri for http://fossbros-anonymous.io/db22128d-884e-4358-9935-6a7c3940535d") } func (suite *ASToInternalTestSuite) TestParseFlag5() { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go index 0ffad6fc1..c88fd2e11 100644 --- a/internal/typeutils/internaltoas.go +++ b/internal/typeutils/internaltoas.go @@ -1713,7 +1713,7 @@ func (c *Converter) PollVoteToASCreate( ap.MustSet(ap.SetJSONLDIdStr, ap.WithJSONLDId(create), id) // Set Create actor appropriately. - ap.AppendActor(create, authorIRI) + ap.AppendActorIRIs(create, authorIRI) // Set publish time for activity. ap.SetPublished(create, vote.CreatedAt) diff --git a/internal/typeutils/util.go b/internal/typeutils/util.go index 8a8d4123b..2eeada868 100644 --- a/internal/typeutils/util.go +++ b/internal/typeutils/util.go @@ -19,7 +19,6 @@ import ( "context" - "errors" "fmt" "net/url" "path" @@ -27,7 +26,6 @@ "strconv" "strings" - "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -94,22 +92,6 @@ func misskeyReportInlineURLs(content string) []*url.URL { return urls } -// getURI is a shortcut/util function for extracting -// the JSONLDId URI of an Activity or Object. -func getURI(withID ap.WithJSONLDId) (*url.URL, string, error) { - idProp := withID.GetJSONLDId() - if idProp == nil { - return nil, "", errors.New("id prop was nil") - } - - if !idProp.IsIRI() { - return nil, "", errors.New("id prop was not an IRI") - } - - id := idProp.Get() - return id, id.String(), nil -} - // placeholdUnknownAttachments separates any attachments with type `unknown` // out of the given slice, and returns an `