diff options
author | 2023-11-30 16:22:34 +0000 | |
---|---|---|
committer | 2023-11-30 16:22:34 +0000 | |
commit | eb170003b81504ba6eb85f950c223dc9eaf1cfca (patch) | |
tree | f1f9779e14875faa70f4db85a8cf19100633884d | |
parent | [bugfix] always go through status parent dereferencing on isNew, even on data... (diff) | |
download | gotosocial-eb170003b81504ba6eb85f950c223dc9eaf1cfca.tar.xz |
[bugfix] return 400 Bad Request on more cases of malformed AS data (#2399)
47 files changed, 1443 insertions, 963 deletions
@@ -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 @@ -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 { - return nil - } - id := idProp.Get() - if id == nil { + if idProp == nil || !idProp.IsXMLSchemaAnyURI() { 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 @@ var SentinelError = errors.New("BUG: error should not be returned") //nolint:rev // 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 @@ stackLoop: // - 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 @@ const ( 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 +} + +// 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 } -// 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) +// 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 @@ import ( "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 <http://www.gnu.org/licenses/>. + +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 @@ package middleware 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 @@ package typeutils import ( "context" "errors" - "fmt" "net/url" "github.com/miekg/dns" @@ -38,201 +37,204 @@ import ( // // 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() + + // 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) + } - // Username aka preferredUsername - // We need this one so bail if it's not set. - username, err := ap.ExtractPreferredUsername(accountable) + // 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) + } + + // Extract a preferred name (display name), fallback to username. + if displayName := ap.ExtractName(accountable); displayName != "" { + acct.DisplayName = displayName + } else { + acct.DisplayName = acct.Username } - acct.Username = username - // Domain + // 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 + // Extract 'manuallyApprovesFollowers', (i.e. locked account) + maf := accountable.GetActivityStreamsManuallyApprovesFollowers() - // assume not sensitive (todo) - sensitive := false - acct.Sensitive = &sensitive + switch { + case maf != nil && !maf.IsXMLSchemaBoolean(): + log.Warnf(ctx, "unusable manuallyApprovesFollowers for %s", uri) + fallthrough - // assume not hide collections (todo) - hideCollections := false - acct.HideCollections = &hideCollections + case maf == nil: + // None given, use default. + acct.Locked = util.Ptr(true) - // locked aka manuallyApprovesFollowers - locked := true - acct.Locked = &locked // assume locked by default - maf := accountable.GetActivityStreamsManuallyApprovesFollowers() - if maf != nil && maf.IsXMLSchemaBoolean() { - locked = maf.Get() + 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) - 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) + status.Account, err = c.getASAttributedToAccount(ctx, + status.URI, + statusable, + ) 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") + uriObj := ap.GetJSONLDId(followable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - 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) - } + // Stringify uri obj. + uri := uriObj.String() - target, err := ap.ExtractObjectURI(followable) + 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") + uriObj := ap.GetJSONLDId(followable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - 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) - } + // Stringify uri obj. + uri := uriObj.String() - target, err := ap.ExtractObjectURI(followable) + 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") + uriObj := ap.GetJSONLDId(likeable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - 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) - } + // Stringify uri obj. + uri := uriObj.String() - target, err := ap.ExtractObjectURI(likeable) + 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") + uriObj := ap.GetJSONLDId(blockable) + if uriObj == nil { + err := gtserror.New("unusable iri property") + return nil, gtserror.SetMalformed(err) } - 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) - } + // Stringify uri obj. + uri := uriObj.String() - target, err := ap.ExtractObjectURI(blockable) + 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. - isNew = true - - // Start assembling the new status - // (we already know the URI). + // Create DB status with URI status = new(gtsmodel.Status) - status.URI = statusURIStr + status.URI = uri + isNew = true // 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 - - // Extract published time for the boost. - published, err := ap.ExtractPublished(announceable) - if err != nil { - err = gtserror.Newf("error extracting published: %w", err) - return nil, isNew, err + status.BoostOf = new(gtsmodel.Status) + status.BoostOf.URI = boostOf[0].String() + + // 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) } - status.CreatedAt = published - status.UpdatedAt = published - // Extract URI of the boosting account. - accountURI, err := ap.ExtractActorURI(announceable) + // 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 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) - if err != nil { - return nil, fmt.Errorf("ASFlagToReport: error extracting actor: %w", err) - } - account, err := c.state.DB.GetAccountByURI(ctx, actor.String()) + // 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 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") + // Ensure we have a target. + if targetAccURI == nil { + err := gtserror.Newf("missing target account uri for %s", uri) + return nil, gtserror.SetMalformed(err) } - targetAccount, err := c.state.DB.GetAccountByURI(ctx, targetAccountURI.String()) + + // Fetch target account from the database by its URI. + targetAcc, err := c.state.DB.GetAccountByURI(ctx, targetAccURI.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) + 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() + // Rescope as just the URI string. + statusURI := 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) - } + // 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 + } - 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) - } + 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 + } - 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 @@ package typeutils import ( "context" - "errors" "fmt" "net/url" "path" @@ -27,7 +26,6 @@ import ( "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 `<aside>` tag containing links to // those attachments, as well as the slice of remaining "known" attachments. diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go index 5892e0a66..89bcdfc09 100644 --- a/internal/typeutils/wrap.go +++ b/internal/typeutils/wrap.go @@ -103,7 +103,7 @@ func wrapStatusableInActivity(activity ap.Activityable, status ap.Statusable, ir appendStatusableToActivity(activity, status, iriOnly) ap.AppendTo(activity, ap.GetTo(status)...) ap.AppendCc(activity, ap.GetCc(status)...) - ap.AppendActor(activity, ap.GetAttributedTo(status)...) + ap.AppendActorIRIs(activity, ap.GetAttributedTo(status)...) ap.SetPublished(activity, ap.GetPublished(status)) } diff --git a/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go b/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go index 5756c0cf5..eded6e7f4 100644 --- a/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go +++ b/vendor/codeberg.org/gruf/go-cache/v3/result/cache.go @@ -2,8 +2,6 @@ package result import ( "context" - "fmt" - "os" "reflect" _ "unsafe" @@ -11,8 +9,6 @@ import ( "codeberg.org/gruf/go-errors/v2" ) -var ErrUnsupportedZero = errors.New("") - // Lookup represents a struct object lookup method in the cache. type Lookup struct { // Name is a period ('.') separated string @@ -58,7 +54,8 @@ func New[T any](lookups []Lookup, copy func(T) T, cap int) *Cache[T] { } // Allocate new cache object - c := &Cache[T]{copy: copy} + c := new(Cache[T]) + c.copy = copy // use copy fn. c.lookups = make([]structKey, len(lookups)) for i, lookup := range lookups { @@ -96,7 +93,7 @@ func (c *Cache[T]) SetEvictionCallback(hook func(T)) { } // Free result and call hook. - v := getResultValue[T](res) + v := res.Value.(T) putResult(res) hook(v) }) @@ -125,7 +122,7 @@ func (c *Cache[T]) SetInvalidateCallback(hook func(T)) { } // Free result and call hook. - v := getResultValue[T](res) + v := res.Value.(T) putResult(res) hook(v) }) @@ -135,11 +132,8 @@ func (c *Cache[T]) SetInvalidateCallback(hook func(T)) { func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) { if ignore == nil { ignore = func(err error) bool { - return errors.Comparable( - err, - context.Canceled, - context.DeadlineExceeded, - ) + return errors.Is(err, context.Canceled) || + errors.Is(err, context.DeadlineExceeded) } } c.cache.Lock() @@ -149,23 +143,83 @@ func (c *Cache[T]) IgnoreErrors(ignore func(error) bool) { // Load will attempt to load an existing result from the cacche for the given lookup and key parts, else calling the provided load function and caching the result. func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) (T, error) { - var zero T - var res *result + info := c.lookups.get(lookup) + key := info.genKey(keyParts) + return c.load(info, key, load) +} + +// Has checks the cache for a positive result under the given lookup and key parts. +func (c *Cache[T]) Has(lookup string, keyParts ...any) bool { + info := c.lookups.get(lookup) + key := info.genKey(keyParts) + return c.has(info, key) +} - // Get lookup key info by name. - keyInfo := c.lookups.get(lookup) - if !keyInfo.unique { - panic("non-unique lookup does not support load: " + lookup) +// Store will call the given store function, and on success store the value in the cache as a positive result. +func (c *Cache[T]) Store(value T, store func() error) error { + // Attempt to store this value. + if err := store(); err != nil { + return err } - // Generate cache key string. - ckey := keyInfo.genKey(keyParts) + // Prepare cached result. + result := getResult() + result.Keys = c.lookups.generate(value) + result.Value = c.copy(value) + result.Error = nil + + var evict func() + + // Lock cache. + c.cache.Lock() + + defer func() { + // Unlock cache. + c.cache.Unlock() + + if evict != nil { + // Call evict. + evict() + } + + // Call invalidate. + c.invalid(value) + }() + + // Store result in cache. + evict = c.store(result) + + return nil +} + +// Invalidate will invalidate any result from the cache found under given lookup and key parts. +func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) { + info := c.lookups.get(lookup) + key := info.genKey(keyParts) + c.invalidate(info, key) +} + +// Clear empties the cache, calling the invalidate callback where necessary. +func (c *Cache[T]) Clear() { c.Trim(100) } + +// Trim ensures the cache stays within percentage of total capacity, truncating where necessary. +func (c *Cache[T]) Trim(perc float64) { c.cache.Trim(perc) } + +func (c *Cache[T]) load(lookup *structKey, key string, load func() (T, error)) (T, error) { + if !lookup.unique { // ensure this lookup only returns 1 result + panic("non-unique lookup does not support load: " + lookup.name) + } + + var ( + zero T + res *result + ) // Acquire cache lock c.cache.Lock() // Look for primary key for cache key (only accept len=1) - if pkeys := keyInfo.pkeys[ckey]; len(pkeys) == 1 { + if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { // Fetch the result for primary key entry, ok := c.cache.Cache.Get(pkeys[0]) @@ -200,8 +254,8 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) // This load returned an error, only // store this item under provided key. res.Keys = []cacheKey{{ - info: keyInfo, - key: ckey, + info: lookup, + key: key, }} } else { // Alloc result. @@ -239,66 +293,19 @@ func (c *Cache[T]) Load(lookup string, load func() (T, error), keyParts ...any) } // Copy value from cached result. - v := c.copy(getResultValue[T](res)) + v := c.copy(res.Value.(T)) return v, nil } -// Store will call the given store function, and on success store the value in the cache as a positive result. -func (c *Cache[T]) Store(value T, store func() error) error { - // Attempt to store this value. - if err := store(); err != nil { - return err - } - - // Prepare cached result. - result := getResult() - result.Keys = c.lookups.generate(value) - result.Value = c.copy(value) - result.Error = nil - - var evict func() - - // Lock cache. - c.cache.Lock() - - defer func() { - // Unlock cache. - c.cache.Unlock() - - if evict != nil { - // Call evict. - evict() - } - - // Call invalidate. - c.invalid(value) - }() - - // Store result in cache. - evict = c.store(result) - - return nil -} - -// Has checks the cache for a positive result under the given lookup and key parts. -func (c *Cache[T]) Has(lookup string, keyParts ...any) bool { +func (c *Cache[T]) has(lookup *structKey, key string) bool { var res *result - // Get lookup key info by name. - keyInfo := c.lookups.get(lookup) - if !keyInfo.unique { - panic("non-unique lookup does not support has: " + lookup) - } - - // Generate cache key string. - ckey := keyInfo.genKey(keyParts) - // Acquire cache lock c.cache.Lock() // Look for primary key for cache key (only accept len=1) - if pkeys := keyInfo.pkeys[ckey]; len(pkeys) == 1 { + if pkeys := lookup.pkeys[key]; len(pkeys) == 1 { // Fetch the result for primary key entry, ok := c.cache.Cache.Get(pkeys[0]) @@ -320,31 +327,6 @@ func (c *Cache[T]) Has(lookup string, keyParts ...any) bool { return ok } -// Invalidate will invalidate any result from the cache found under given lookup and key parts. -func (c *Cache[T]) Invalidate(lookup string, keyParts ...any) { - // Get lookup key info by name. - keyInfo := c.lookups.get(lookup) - - // Generate cache key string. - ckey := keyInfo.genKey(keyParts) - - // Look for primary key for cache key - c.cache.Lock() - pkeys := keyInfo.pkeys[ckey] - delete(keyInfo.pkeys, ckey) - c.cache.Unlock() - - // Invalidate all primary keys. - c.cache.InvalidateAll(pkeys...) -} - -// Clear empties the cache, calling the invalidate callback where necessary. -func (c *Cache[T]) Clear() { c.Trim(100) } - -// Trim ensures the cache stays within percentage of total capacity, truncating where necessary. -func (c *Cache[T]) Trim(perc float64) { c.cache.Trim(perc) } - -// store will cache this result under all of its required cache keys. func (c *Cache[T]) store(res *result) (evict func()) { var toEvict []*result @@ -425,6 +407,17 @@ func (c *Cache[T]) store(res *result) (evict func()) { } } +func (c *Cache[T]) invalidate(lookup *structKey, key string) { + // Look for primary key for cache key + c.cache.Lock() + pkeys := lookup.pkeys[key] + delete(lookup.pkeys, key) + c.cache.Unlock() + + // Invalidate all primary keys. + c.cache.InvalidateAll(pkeys...) +} + type result struct { // Result primary key PKey int64 @@ -438,12 +431,3 @@ type result struct { // cached error Error error } - -// getResultValue is a safe way of casting and fetching result value. -func getResultValue[T any](res *result) T { - v, ok := res.Value.(T) - if !ok { - fmt.Fprintf(os.Stderr, "!! BUG: unexpected value type in result: %T\n", res.Value) - } - return v -} diff --git a/vendor/codeberg.org/gruf/go-cache/v3/result/key.go b/vendor/codeberg.org/gruf/go-cache/v3/result/key.go index 5e10e6fa1..5508936ba 100644 --- a/vendor/codeberg.org/gruf/go-cache/v3/result/key.go +++ b/vendor/codeberg.org/gruf/go-cache/v3/result/key.go @@ -43,9 +43,8 @@ func (sk structKeys) generate(a any) []cacheKey { v = v.Elem() } - // Acquire byte buffer + // Acquire buffer buf := getBuf() - defer putBuf(buf) outer: for i := range sk { @@ -80,6 +79,9 @@ outer: }) } + // Release buf + putBuf(buf) + return keys } @@ -124,11 +126,12 @@ type structKey struct { unique bool // fields is a slice of runtime struct field - // indices, of the fields encompassed by this key. + // indices, of fields encompassed by this key. fields []structField // pkeys is a lookup of stored struct key values - // to the primary cache lookup key (int64). + // to the primary cache lookup key (int64). this + // is protected by the main cache mutex. pkeys map[string][]int64 } @@ -192,9 +195,8 @@ func (sk *structKey) genKey(parts []any) string { panic(fmt.Sprintf("incorrect no. key parts provided: want=%d received=%d", len(parts), len(sk.fields))) } - // Acquire byte buffer + // Acquire buffer buf := getBuf() - defer putBuf(buf) buf.Reset() for i, part := range parts { @@ -210,8 +212,13 @@ func (sk *structKey) genKey(parts []any) string { // Drop last '.' buf.Truncate(1) - // Return string copy - return string(buf.B) + // Create str copy + str := string(buf.B) + + // Release buf + putBuf(buf) + + return str } type structField struct { diff --git a/vendor/codeberg.org/gruf/go-errors/v2/README.md b/vendor/codeberg.org/gruf/go-errors/v2/README.md index 90049abec..21f7cb767 100644 --- a/vendor/codeberg.org/gruf/go-errors/v2/README.md +++ b/vendor/codeberg.org/gruf/go-errors/v2/README.md @@ -1,5 +1,8 @@ # go-errors -simple but powerful errors library that allows easy wrapping and stacktracing of errors. - -to disable stacktraces set the `notrace` build tag.
\ No newline at end of file +powerful errors library with a simple API that allows: +- accessing all the standard library errors functions +- wrapping of errors +- adding values to errors, in a similar manner to contexts +- including calling function prefix when tag=errcaller is set +- including stacktrace with error when tag=errtrace is set diff --git a/vendor/codeberg.org/gruf/go-errors/v2/build_caller.go b/vendor/codeberg.org/gruf/go-errors/v2/build_caller.go new file mode 100644 index 000000000..ad5067574 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-errors/v2/build_caller.go @@ -0,0 +1,26 @@ +//go:build errcaller +// +build errcaller + +package errors + +import ( + _ "unsafe" +) + +// IncludesCaller is a compile-time flag used to indicate whether +// to include calling function prefix on error wrap / creation. +const IncludesCaller = true + +type caller string + +// set will set the actual caller value +// only when correct build flag is set. +func (c *caller) set(v string) { + *c = caller(v) +} + +// value returns the actual caller value +// only when correct build flag is set +func (c caller) value() string { + return string(c) +} diff --git a/vendor/codeberg.org/gruf/go-errors/v2/build_nocaller.go b/vendor/codeberg.org/gruf/go-errors/v2/build_nocaller.go new file mode 100644 index 000000000..935283bf5 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-errors/v2/build_nocaller.go @@ -0,0 +1,18 @@ +//go:build !errcaller +// +build !errcaller + +package errors + +// IncludesCaller is a compile-time flag used to indicate whether +// to include calling function prefix on error wrap / creation. +const IncludesCaller = false + +type caller struct{} + +// set will set the actual caller value +// only when correct build flag is set. +func (caller) set(string) {} + +// value returns the actual caller value +// only when correct build flag is set. +func (caller) value() string { return "" } diff --git a/vendor/codeberg.org/gruf/go-errors/v2/build_notrace.go b/vendor/codeberg.org/gruf/go-errors/v2/build_notrace.go new file mode 100644 index 000000000..acee2e698 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-errors/v2/build_notrace.go @@ -0,0 +1,20 @@ +//go:build !errtrace +// +build !errtrace + +package errors + +import "runtime" + +// IncludesStacktrace is a compile-time flag used to indicate +// whether to include stacktraces on error wrap / creation. +const IncludesStacktrace = false + +type trace struct{} + +// set will set the actual trace value +// only when correct build flag is set. +func (trace) set([]runtime.Frame) {} + +// value returns the actual trace value +// only when correct build flag is set. +func (trace) value() Callers { return nil } diff --git a/vendor/codeberg.org/gruf/go-errors/v2/build_trace.go b/vendor/codeberg.org/gruf/go-errors/v2/build_trace.go new file mode 100644 index 000000000..e7b652699 --- /dev/null +++ b/vendor/codeberg.org/gruf/go-errors/v2/build_trace.go @@ -0,0 +1,27 @@ +//go:build errtrace +// +build errtrace + +package errors + +import ( + "runtime" + _ "unsafe" +) + +// IncludesStacktrace is a compile-time flag used to indicate +// whether to include stacktraces on error wrap / creation. +const IncludesStacktrace = true + +type trace []runtime.Frame + +// set will set the actual trace value +// only when correct build flag is set. +func (t *trace) set(v []runtime.Frame) { + *t = trace(v) +} + +// value returns the actual trace value +// only when correct build flag is set. +func (t trace) value() Callers { + return Callers(t) +} diff --git a/vendor/codeberg.org/gruf/go-errors/v2/callers.go b/vendor/codeberg.org/gruf/go-errors/v2/callers.go deleted file mode 100644 index 3fe84b0c5..000000000 --- a/vendor/codeberg.org/gruf/go-errors/v2/callers.go +++ /dev/null @@ -1,98 +0,0 @@ -package errors - -import ( - "encoding/json" - "runtime" - "strconv" - "strings" - "unsafe" -) - -// Callers is a stacktrace of caller PCs. -type Callers []uintptr - -// GetCallers returns a Callers slice of PCs, of at most 'depth'. -func GetCallers(skip int, depth int) Callers { - rpc := make([]uintptr, depth) - n := runtime.Callers(skip+1, rpc) - return Callers(rpc[0:n]) -} - -// Frames fetches runtime frames for a slice of caller PCs. -func (f Callers) Frames() []runtime.Frame { - // Allocate expected frames slice - frames := make([]runtime.Frame, 0, len(f)) - - // Get frames iterator for PCs - iter := runtime.CallersFrames(f) - - for { - // Get next frame in iter - frame, ok := iter.Next() - if !ok { - break - } - - // Append to frames slice - frames = append(frames, frame) - } - - return frames -} - -// MarshalJSON implements json.Marshaler to provide an easy, simple default. -func (f Callers) MarshalJSON() ([]byte, error) { - // JSON-able frame type - type jsonFrame struct { - Func string `json:"func"` - File string `json:"file"` - Line int `json:"line"` - } - - // Convert to frames - frames := f.Frames() - - // Allocate expected size jsonFrame slice - jsonFrames := make([]jsonFrame, 0, len(f)) - - for i := 0; i < len(frames); i++ { - frame := frames[i] - - // Convert each to jsonFrame object - jsonFrames = append(jsonFrames, jsonFrame{ - Func: funcname(frame.Function), - File: frame.File, - Line: frame.Line, - }) - } - - // marshal converted frames - return json.Marshal(frames) -} - -// String will return a simple string representation of receiving Callers slice. -func (f Callers) String() string { - // Guess-timate to reduce allocs - buf := make([]byte, 0, 64*len(f)) - - // Convert to frames - frames := f.Frames() - - for i := 0; i < len(frames); i++ { - frame := frames[i] - - // Append formatted caller info - fn := funcname(frame.Function) - buf = append(buf, fn+"()\n\t"+frame.File+":"...) - buf = strconv.AppendInt(buf, int64(frame.Line), 10) - buf = append(buf, '\n') - } - - return *(*string)(unsafe.Pointer(&buf)) -} - -// funcname splits a function name with pkg from its path prefix. -func funcname(name string) string { - i := strings.LastIndex(name, "/") - return name[i+1:] -} diff --git a/vendor/codeberg.org/gruf/go-errors/v2/error.go b/vendor/codeberg.org/gruf/go-errors/v2/error.go deleted file mode 100644 index ed1217a29..000000000 --- a/vendor/codeberg.org/gruf/go-errors/v2/error.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build !notrace -// +build !notrace - -package errors - -type errormsg struct { - msg string - wrap error - stack Callers -} - -func create(msg string, wrap error) *errormsg { - return &errormsg{ - msg: msg, - wrap: wrap, - stack: GetCallers(2, 10), - } -} - -func (err *errormsg) Error() string { - return err.msg -} - -func (err *errormsg) Is(target error) bool { - other, ok := target.(*errormsg) - return ok && (err.msg == other.msg) -} - -func (err *errormsg) Unwrap() error { - return err.wrap -} - -func (err *errormsg) Stacktrace() Callers { - return err.stack -} diff --git a/vendor/codeberg.org/gruf/go-errors/v2/error_notrace.go b/vendor/codeberg.org/gruf/go-errors/v2/error_notrace.go deleted file mode 100644 index e5faf80a2..000000000 --- a/vendor/codeberg.org/gruf/go-errors/v2/error_notrace.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build notrace -// +build notrace - -package errors - -type errormsg struct { - msg string - wrap error -} - -func create(msg string, wrap error) *errormsg { - return &errormsg{ - msg: msg, - wrap: wrap, - } -} - -func (err *errormsg) Error() string { - return err.msg -} - -func (err *errormsg) Is(target error) bool { - other, ok := target.(*errormsg) - return ok && (err.msg == other.msg) -} - -func (err *errormsg) Unwrap() error { - return err.wrap -} - -func (err *errormsg) Stacktrace() Callers { - return nil -} diff --git a/vendor/codeberg.org/gruf/go-errors/v2/errors.go b/vendor/codeberg.org/gruf/go-errors/v2/errors.go index 2c4689151..d5f1a7ab1 100644 --- a/vendor/codeberg.org/gruf/go-errors/v2/errors.go +++ b/vendor/codeberg.org/gruf/go-errors/v2/errors.go @@ -1,37 +1,248 @@ package errors import ( - "errors" "fmt" + "runtime" ) // New returns a new error created from message. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline func New(msg string) error { - return create(msg, nil) + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(2, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(2, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errormsg{ + cfn: c, + msg: msg, + trc: t, + } } // Newf returns a new error created from message format and args. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline func Newf(msgf string, args ...interface{}) error { - return create(fmt.Sprintf(msgf, args...), nil) + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(2, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(2, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errormsg{ + cfn: c, + msg: fmt.Sprintf(msgf, args...), + trc: t, + } +} + +// NewAt returns a new error created, skipping 'skip' +// frames for trace / caller information, from message. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline +func NewAt(skip int, msg string) error { + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(skip+1, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(skip+1, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errormsg{ + cfn: c, + msg: msg, + trc: t, + } } // Wrap will wrap supplied error within a new error created from message. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline func Wrap(err error, msg string) error { - return create(msg, err) + if err == nil { + panic("cannot wrap nil error") + } + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(2, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(2, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errorwrap{ + cfn: c, + msg: msg, + err: err, + trc: t, + } } // Wrapf will wrap supplied error within a new error created from message format and args. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline func Wrapf(err error, msgf string, args ...interface{}) error { - return create(fmt.Sprintf(msgf, args...), err) + if err == nil { + panic("cannot wrap nil error") + } + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(2, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(2, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errorwrap{ + cfn: c, + msg: fmt.Sprintf(msgf, args...), + err: err, + trc: t, + } +} + +// WrapAt wraps error within new error created from message, +// skipping 'skip' frames for trace / caller information. +// +// Note this function cannot be inlined, to ensure expected +// and consistent behaviour in setting trace / caller info. +// +//go:noinline +func WrapAt(skip int, err error, msg string) error { + if err == nil { + panic("cannot wrap nil error") + } + var c caller + var t trace + if IncludesCaller { + pcs := make([]uintptr, 1) + _ = runtime.Callers(skip+1, pcs) + fn := runtime.FuncForPC(pcs[0]) + c.set(funcName(fn)) + } + if IncludesStacktrace { + pcs := make([]uintptr, 10) + n := runtime.Callers(skip+1, pcs) + iter := runtime.CallersFrames(pcs[:n]) + t.set(gatherFrames(iter, n)) + } + return &_errorwrap{ + cfn: c, + msg: msg, + err: err, + trc: t, + } } // Stacktrace fetches first stored stacktrace of callers from error chain. func Stacktrace(err error) Callers { - var e interface { - Stacktrace() Callers - } - if !errors.As(err, &e) { + if !IncludesStacktrace { + // compile-time check return nil } - return e.Stacktrace() + if e := AsV2[*_errormsg](err); err != nil { + return e.trc.value() + } + if e := AsV2[*_errorwrap](err); err != nil { + return e.trc.value() + } + return nil +} + +type _errormsg struct { + cfn caller + msg string + trc trace +} + +func (err *_errormsg) Error() string { + if IncludesCaller { + fn := err.cfn.value() + return fn + " " + err.msg + } else { + return err.msg + } +} + +func (err *_errormsg) Is(other error) bool { + oerr, ok := other.(*_errormsg) + return ok && oerr.msg == err.msg +} + +type _errorwrap struct { + cfn caller + msg string + err error // wrapped + trc trace +} + +func (err *_errorwrap) Error() string { + if IncludesCaller { + fn := err.cfn.value() + return fn + " " + err.msg + ": " + err.err.Error() + } else { + return err.msg + ": " + err.err.Error() + } +} + +func (err *_errorwrap) Is(other error) bool { + oerr, ok := other.(*_errorwrap) + return ok && oerr.msg == err.msg +} + +func (err *_errorwrap) Unwrap() error { + return err.err } diff --git a/vendor/codeberg.org/gruf/go-errors/v2/once.go b/vendor/codeberg.org/gruf/go-errors/v2/once.go index 83a45a61f..467fe726d 100644 --- a/vendor/codeberg.org/gruf/go-errors/v2/once.go +++ b/vendor/codeberg.org/gruf/go-errors/v2/once.go @@ -2,46 +2,30 @@ package errors import ( "sync/atomic" - "unsafe" ) // OnceError is an error structure that supports safe multi // threaded usage and setting only once (until reset). -type OnceError struct{ err unsafe.Pointer } - -// NewOnce returns a new OnceError instance. -func NewOnce() OnceError { - return OnceError{ - err: nil, - } -} +type OnceError struct{ ptr atomic.Pointer[error] } // Store will safely set the OnceError to value, no-op if nil. -func (e *OnceError) Store(err error) { - // Nothing to do +func (e *OnceError) Store(err error) bool { if err == nil { - return + return false } - - // Only set if not already - atomic.CompareAndSwapPointer( - &e.err, - nil, - unsafe.Pointer(&err), - ) + return e.ptr.CompareAndSwap(nil, &err) } // Load will load the currently stored error. func (e *OnceError) Load() error { - return *(*error)(atomic.LoadPointer(&e.err)) + if ptr := e.ptr.Load(); ptr != nil { + return *ptr + } + return nil } // IsSet returns whether OnceError has been set. -func (e *OnceError) IsSet() bool { - return (atomic.LoadPointer(&e.err) != nil) -} +func (e *OnceError) IsSet() bool { return (e.ptr.Load() != nil) } // Reset will reset the OnceError value. -func (e *OnceError) Reset() { - atomic.StorePointer(&e.err, nil) -} +func (e *OnceError) Reset() { e.ptr.Store(nil) } diff --git a/vendor/codeberg.org/gruf/go-errors/v2/runtime.go b/vendor/codeberg.org/gruf/go-errors/v2/runtime.go new file mode 100644 index 000000000..0c8cf11cd --- /dev/null +++ b/vendor/codeberg.org/gruf/go-errors/v2/runtime.go @@ -0,0 +1,97 @@ +package errors + +import ( + "encoding/json" + "runtime" + "strconv" + "strings" + "unsafe" +) + +// Callers ... +type Callers []runtime.Frame + +// MarshalJSON implements json.Marshaler to provide an easy, simple default. +func (c Callers) MarshalJSON() ([]byte, error) { + // JSON-able frame type + type jsonFrame struct { + Func string `json:"func"` + File string `json:"file"` + Line int `json:"line"` + } + + // Allocate expected size jsonFrame slice + jsonFrames := make([]jsonFrame, len(c)) + + // Convert each to jsonFrame object + for i := 0; i < len(c); i++ { + frame := c[i] + jsonFrames[i] = jsonFrame{ + Func: funcName(frame.Func), + File: frame.File, + Line: frame.Line, + } + } + + // marshal converted frames + return json.Marshal(jsonFrames) +} + +// String will return a simple string representation of receiving Callers slice. +func (c Callers) String() string { + // Guess-timate to reduce allocs + buf := make([]byte, 0, 64*len(c)) + + for i := 0; i < len(c); i++ { + frame := c[i] + + // Append formatted caller info + fn := funcName(frame.Func) + buf = append(buf, fn+"()\n\t"+frame.File+":"...) + buf = strconv.AppendInt(buf, int64(frame.Line), 10) + buf = append(buf, '\n') + } + + return *(*string)(unsafe.Pointer(&buf)) +} + +// funcName formats a function name to a quickly-readable string. +func funcName(fn *runtime.Func) string { + if fn == nil { + return "" + } + + // Get func name + // for formatting. + name := fn.Name() + + // Drop all but the package name and function name, no mod path + if idx := strings.LastIndex(name, "/"); idx >= 0 { + name = name[idx+1:] + } + + const params = `[...]` + + // Drop any generic type parameter markers + if idx := strings.Index(name, params); idx >= 0 { + name = name[:idx] + name[idx+len(params):] + } + + return name +} + +// gatherFrames collates runtime frames from a frame iterator. +func gatherFrames(iter *runtime.Frames, n int) Callers { + 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/vendor/codeberg.org/gruf/go-errors/v2/standard.go b/vendor/codeberg.org/gruf/go-errors/v2/standard.go index 1d2c71c5f..3739416dc 100644 --- a/vendor/codeberg.org/gruf/go-errors/v2/standard.go +++ b/vendor/codeberg.org/gruf/go-errors/v2/standard.go @@ -1,133 +1,37 @@ package errors import ( - "errors" - "reflect" _ "unsafe" - - "codeberg.org/gruf/go-bitutil" ) -// errtype is a ptr to the error interface type. -var errtype = reflect.TypeOf((*error)(nil)).Elem() - -// Comparable is functionally equivalent to calling errors.Is() on multiple errors (up to a max of 64). -func Comparable(err error, targets ...error) bool { - var flags bitutil.Flags64 - - // Flags only has 64 bit-slots - if len(targets) > 64 { - panic("too many targets") - } - - for i := 0; i < len(targets); { - if targets[i] == nil { - if err == nil { - return true - } - - // Drop nil targets from slice. - copy(targets[i:], targets[i+1:]) - targets = targets[:len(targets)-1] - continue - } - - // Check if this error is directly comparable - if reflect.TypeOf(targets[i]).Comparable() { - flags = flags.Set(uint8(i)) - } - - i++ - } - - for err != nil { - // Check if this layer supports .Is interface - is, ok := err.(interface{ Is(error) bool }) - - if !ok { - // Error does not support interface - // - // Only try perform direct compare - for i := 0; i < len(targets); i++ { - // Try directly compare errors - if flags.Get(uint8(i)) && - err == targets[i] { - return true - } - } - } else { - // Error supports the .Is interface - // - // Perform direct compare AND .Is() - for i := 0; i < len(targets); i++ { - if (flags.Get(uint8(i)) && - err == targets[i]) || - is.Is(targets[i]) { - return true - } - } - } - - // Unwrap to next layer - err = errors.Unwrap(err) - } - - return false -} - -// Assignable is functionally equivalent to calling errors.As() on multiple errors, -// except that it only checks assignability as opposed to setting the target. -func Assignable(err error, targets ...error) bool { - if err == nil { - // Fastest case. - return false - } - - for i := 0; i < len(targets); { - if targets[i] == nil { - // Drop nil targets from slice. - copy(targets[i:], targets[i+1:]) - targets = targets[:len(targets)-1] - continue - } - i++ - } - - for err != nil { - // Check if this layer supports .As interface - as, ok := err.(interface{ As(any) bool }) - - // Get reflected err type. - te := reflect.TypeOf(err) - - if !ok { - // Error does not support interface. - // - // Check assignability using reflection. - for i := 0; i < len(targets); i++ { - tt := reflect.TypeOf(targets[i]) - if te.AssignableTo(tt) { - return true - } - } - } else { - // Error supports the .As interface. - // - // Check using .As() and reflection. - for i := 0; i < len(targets); i++ { - if as.As(targets[i]) { - return true - } else if tt := reflect.TypeOf(targets[i]); // nocollapse - te.AssignableTo(tt) { - return true - } - } +// Is reports whether any error in err's tree matches target. +// +// The tree consists of err itself, followed by the errors obtained by repeatedly +// calling Unwrap. When err wraps multiple errors, Is examines err followed by a +// depth-first traversal of its children. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// +// An error type might provide an Is method so it can be treated as equivalent +// to an existing error. For example, if MyError defines +// +// func (m MyError) Is(target error) bool { return target == fs.ErrExist } +// +// then Is(MyError{}, fs.ErrExist) returns true. See [syscall.Errno.Is] for +// an example in the standard library. An Is method should only shallowly +// compare err and the target and not call Unwrap on either. +// +//go:linkname Is errors.Is +func Is(err error, target error) bool + +// IsV2 calls Is(err, target) for each target within targets. +func IsV2(err error, targets ...error) bool { + for _, target := range targets { + if Is(err, target) { + return true } - - // Unwrap to next layer. - err = errors.Unwrap(err) } - return false } @@ -152,8 +56,79 @@ func Assignable(err error, targets ...error) bool { //go:linkname As errors.As func As(err error, target any) bool +// AsV2 is functionally similar to As(), instead +// leveraging generics to handle allocation and +// returning of a concrete generic parameter type. +func AsV2[Type any](err error) Type { + var t Type + var ok bool + errs := []error{err} + for len(errs) > 0 { + // Pop next error to check. + err := errs[len(errs)-1] + errs = errs[:len(errs)-1] + + // Check direct type. + t, ok = err.(Type) + if ok { + return t + } + + // Look for .As() support. + as, ok := err.(interface { + As(target any) bool + }) + + if ok { + // Attempt .As(). + if as.As(&t) { + return t + } + } + + // Try unwrap errors. + switch u := err.(type) { + case interface{ Unwrap() error }: + errs = append(errs, u.Unwrap()) + case interface{ Unwrap() []error }: + errs = append(errs, u.Unwrap()...) + } + } + return t +} + // Unwrap returns the result of calling the Unwrap method on err, if err's -// type contains an Unwrap method returning error. Otherwise, Unwrap returns nil. +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +// +// Unwrap only calls a method of the form "Unwrap() error". +// In particular Unwrap does not unwrap errors returned by [Join]. // //go:linkname Unwrap errors.Unwrap func Unwrap(err error) error + +// UnwrapV2 is functionally similar to Unwrap(), except that +// it also handles the case of interface{ Unwrap() []error }. +func UnwrapV2(err error) []error { + switch u := err.(type) { + case interface{ Unwrap() error }: + if e := u.Unwrap(); err != nil { + return []error{e} + } + case interface{ Unwrap() []error }: + return u.Unwrap() + } + return nil +} + +// Join returns an error that wraps the given errors. +// Any nil error values are discarded. +// Join returns nil if every value in errs is nil. +// The error formats as the concatenation of the strings obtained +// by calling the Error method of each element of errs, with a newline +// between each string. +// +// A non-nil error returned by Join implements the Unwrap() []error method. +// +//go:linkname Join errors.Join +func Join(errs ...error) error diff --git a/vendor/codeberg.org/gruf/go-errors/v2/value.go b/vendor/codeberg.org/gruf/go-errors/v2/value.go index 6d7ec3a25..876e5f80e 100644 --- a/vendor/codeberg.org/gruf/go-errors/v2/value.go +++ b/vendor/codeberg.org/gruf/go-errors/v2/value.go @@ -1,56 +1,50 @@ package errors -import "errors" - // WithValue wraps err to store given key-value pair, accessible via Value() function. func WithValue(err error, key any, value any) error { if err == nil { panic("nil error") } - return &errWithValue{ + var kvs []kv + if e := AsV2[*errWithValues](err); e != nil { + kvs = e.kvs + } + return &errWithValues{ err: err, - key: key, - val: value, + kvs: append(kvs, kv{key, value}), } } // Value searches for value stored under given key in error chain. func Value(err error, key any) any { - var e *errWithValue - - if !errors.As(err, &e) { - return nil + if e := AsV2[*errWithValues](err); e != nil { + return e.Value(key) } - - return e.Value(key) + return nil } -type errWithValue struct { +// simple key-value type. +type kv struct{ k, v any } + +// errWithValues wraps an error to provide key-value storage. +type errWithValues struct { err error - key any - val any + kvs []kv } -func (e *errWithValue) Error() string { +func (e *errWithValues) Error() string { return e.err.Error() } -func (e *errWithValue) Is(target error) bool { - return e.err == target +func (e *errWithValues) Unwrap() error { + return e.err } -func (e *errWithValue) Unwrap() error { - return Unwrap(e.err) -} - -func (e *errWithValue) Value(key any) any { - for { - if key == e.key { - return e.val - } - - if !errors.As(e.err, &e) { - return nil +func (e *errWithValues) Value(key any) any { + for i := range e.kvs { + if e.kvs[i].k == key { + return e.kvs[i].v } } + return nil } diff --git a/vendor/codeberg.org/gruf/go-runners/pool.go b/vendor/codeberg.org/gruf/go-runners/pool.go index 3d9105986..644cde0b9 100644 --- a/vendor/codeberg.org/gruf/go-runners/pool.go +++ b/vendor/codeberg.org/gruf/go-runners/pool.go @@ -221,8 +221,15 @@ func worker_run(ctx context.Context, fns <-chan WorkerFunc) bool { defer func() { // Recover and drop any panic if r := recover(); r != nil { + + // Gather calling func frames. + pcs := make([]uintptr, 10) + n := runtime.Callers(3, pcs) + i := runtime.CallersFrames(pcs[:n]) + c := gatherFrames(i, n) + const msg = "worker_run: recovered panic: %v\n\n%s\n" - fmt.Fprintf(os.Stderr, msg, r, errors.GetCallers(2, 10)) + fmt.Fprintf(os.Stderr, msg, r, c.String()) } }() @@ -243,8 +250,15 @@ func drain_queue(fns <-chan WorkerFunc) bool { defer func() { // Recover and drop any panic if r := recover(); r != nil { - const msg = "drain_queue: recovered panic: %v\n\n%s\n" - fmt.Fprintf(os.Stderr, msg, r, errors.GetCallers(2, 10)) + + // Gather calling func frames. + pcs := make([]uintptr, 10) + n := runtime.Callers(3, pcs) + i := runtime.CallersFrames(pcs[:n]) + c := gatherFrames(i, n) + + const msg = "worker_run: recovered panic: %v\n\n%s\n" + fmt.Fprintf(os.Stderr, msg, r, c.String()) } }() @@ -260,3 +274,19 @@ func drain_queue(fns <-chan WorkerFunc) bool { } } } + +// gatherFrames collates runtime frames from a frame iterator. +func gatherFrames(iter *runtime.Frames, n int) errors.Callers { + 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/vendor/modules.txt b/vendor/modules.txt index bdf6e5d39..32ae1af05 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -13,7 +13,7 @@ codeberg.org/gruf/go-bytesize # codeberg.org/gruf/go-byteutil v1.2.0 ## explicit; go 1.16 codeberg.org/gruf/go-byteutil -# codeberg.org/gruf/go-cache/v3 v3.5.6 +# codeberg.org/gruf/go-cache/v3 v3.5.7 ## explicit; go 1.19 codeberg.org/gruf/go-cache/v3 codeberg.org/gruf/go-cache/v3/result @@ -22,7 +22,7 @@ codeberg.org/gruf/go-cache/v3/ttl # codeberg.org/gruf/go-debug v1.3.0 ## explicit; go 1.16 codeberg.org/gruf/go-debug -# codeberg.org/gruf/go-errors/v2 v2.2.0 +# codeberg.org/gruf/go-errors/v2 v2.3.1 ## explicit; go 1.19 codeberg.org/gruf/go-errors/v2 # codeberg.org/gruf/go-fastcopy v1.1.2 @@ -50,7 +50,7 @@ codeberg.org/gruf/go-maps # codeberg.org/gruf/go-mutexes v1.3.1 ## explicit; go 1.14 codeberg.org/gruf/go-mutexes -# codeberg.org/gruf/go-runners v1.6.1 +# codeberg.org/gruf/go-runners v1.6.2 ## explicit; go 1.19 codeberg.org/gruf/go-runners # codeberg.org/gruf/go-sched v1.2.3 |