diff options
author | 2025-02-19 18:09:54 +0100 | |
---|---|---|
committer | 2025-02-19 18:09:54 +0100 | |
commit | 96716e4f43341beb3431a7caad10d48e6ca844ae (patch) | |
tree | 49e0771a80c5ecdf2cfc42969036fa6044382209 /internal | |
parent | adds more code comments and some small code formatting tweaks (#3799) (diff) | |
download | gotosocial-96716e4f43341beb3431a7caad10d48e6ca844ae.tar.xz |
[feature] Forward-compatibility with Approval objects (#3807)
* vendor
* [feature] Forward-compatibility with Approval objects
* vendor the thing
* fix leetle bug
* lil syntax tweak for beloved kimb
Diffstat (limited to 'internal')
-rw-r--r-- | internal/ap/activitystreams.go | 5 | ||||
-rw-r--r-- | internal/ap/interfaces.go | 50 | ||||
-rw-r--r-- | internal/ap/resolve.go | 58 | ||||
-rw-r--r-- | internal/federation/dereferencing/status_permitted.go | 291 | ||||
-rw-r--r-- | internal/federation/federatingdb/accept.go | 173 | ||||
-rw-r--r-- | internal/processing/workers/fromfediapi.go | 10 |
6 files changed, 459 insertions, 128 deletions
diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index 50955ce2c..3851d0efb 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -97,6 +97,11 @@ const ( // Not in the AS spec, just used internally to indicate // that we don't *yet* know what type of Object something is. ObjectUnknown = "Unknown" + + // Extensions and unofficial additions. + ObjectLikeApproval = "LikeApproval" + ObjectReplyApproval = "ReplyApproval" + ObjectAnnounceApproval = "AnnounceApproval" ) // isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity). diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go index fdd5e4a0b..acc799640 100644 --- a/internal/ap/interfaces.go +++ b/internal/ap/interfaces.go @@ -128,16 +128,13 @@ func ToPollOptionable(t vocab.Type) (PollOptionable, bool) { } // IsAccept returns whether AS vocab type name -// is something that can be cast to Accept. +// is something that can be cast to Acceptable. func IsAcceptable(typeName string) bool { return typeName == ActivityAccept } -// ToAcceptable safely tries to cast vocab.Type as vocab.ActivityStreamsAccept. -// -// TODO: Add additional "Accept" types here, eg., "ApproveReply" from -// https://codeberg.org/fediverse/fep/src/branch/main/fep/5624/fep-5624.md -func ToAcceptable(t vocab.Type) (vocab.ActivityStreamsAccept, bool) { +// ToAcceptable safely tries to cast vocab.Type as Acceptable. +func ToAcceptable(t vocab.Type) (Acceptable, bool) { acceptable, ok := t.(vocab.ActivityStreamsAccept) if !ok || !IsAcceptable(t.GetTypeName()) { return nil, false @@ -145,6 +142,28 @@ func ToAcceptable(t vocab.Type) (vocab.ActivityStreamsAccept, bool) { return acceptable, true } +// IsApprovable returns whether AS vocab type name +// is something that can be cast to Approvable. +func IsApprovable(typeName string) bool { + switch typeName { + case ObjectLikeApproval, + ObjectReplyApproval, + ObjectAnnounceApproval: + return true + default: + return false + } +} + +// ToAcceptable safely tries to cast vocab.Type as Approvable. +func ToApprovable(t vocab.Type) (Approvable, bool) { + approvable, ok := t.(Approvable) + if !ok || !IsApprovable(t.GetTypeName()) { + return nil, false + } + return approvable, true +} + // Activityable represents the minimum activitypub interface for representing an 'activity'. // (see: IsActivityable() for types implementing this, though you MUST make sure to check // the typeName as this bare interface may be implementable by non-Activityable types). @@ -247,6 +266,19 @@ type PollOptionable interface { // interface for representing an Accept. type Acceptable interface { Activityable + + WithTarget + WithResult +} + +// Approvable represents the minimum activitypub interface +// for a LikeApproval, ReplyApproval, or AnnounceApproval. +type Approvable interface { + vocab.Type + + WithAttributedTo + WithObject + WithTarget } // Attachmentable represents the minimum activitypub interface for representing a 'mediaAttachment'. (see: IsAttachmentable). @@ -708,3 +740,9 @@ type WithApprovedBy interface { GetGoToSocialApprovedBy() vocab.GoToSocialApprovedByProperty SetGoToSocialApprovedBy(vocab.GoToSocialApprovedByProperty) } + +// WithVotersCount represents an activity or object the result property. +type WithResult interface { + GetActivityStreamsResult() vocab.ActivityStreamsResultProperty + SetActivityStreamsResult(vocab.ActivityStreamsResultProperty) +} diff --git a/internal/ap/resolve.go b/internal/ap/resolve.go index 76a8809c3..82a242710 100644 --- a/internal/ap/resolve.go +++ b/internal/ap/resolve.go @@ -198,48 +198,12 @@ func ResolveCollectionPage(ctx context.Context, body io.ReadCloser) (CollectionP return ToCollectionPageIterator(t) } -// ResolveAcceptable tries to resolve the given reader -// into an ActivityStreams Acceptable representation. -func ResolveAcceptable( - ctx context.Context, - body io.ReadCloser, -) (Acceptable, error) { - // Get "raw" map - // destination. - raw := getMap() - // Release. - defer putMap(raw) - - // Decode data as JSON into 'raw' map - // and get the resolved AS vocab.Type. - // (this handles close of given body). - t, err := decodeType(ctx, body, raw) - if err != nil { - return nil, gtserror.SetWrongType(err) - } - - // Attempt to cast as acceptable. - acceptable, ok := ToAcceptable(t) - if !ok { - err := gtserror.Newf("cannot resolve vocab type %T as acceptable", t) - return nil, gtserror.SetWrongType(err) - } - - return acceptable, nil -} - // emptydest is an empty JSON decode // destination useful for "noop" decodes // to check underlying reader is empty. var emptydest = &struct{}{} -// decodeType tries to read and parse the data -// at provided io.ReadCloser as a JSON ActivityPub -// type, failing if not parseable as JSON or not -// resolveable as one of our known AS types. -// -// NOTE: this function handles closing -// given body when it is finished with. +// decodeType is the package-internal version of DecodeType. // // The given map pointer will also be populated with // the 'raw' JSON data, for further processing. @@ -284,3 +248,23 @@ func decodeType( return t, nil } + +// DecodeType tries to read and parse the data +// at provided io.ReadCloser as a JSON ActivityPub +// type, failing if not parseable as JSON or not +// resolveable as one of our known AS types. +// +// NOTE: this function handles closing +// given body when it is finished with. +func DecodeType( + ctx context.Context, + body io.ReadCloser, +) (vocab.Type, error) { + // Get "raw" map + // destination. + raw := getMap() + // Release. + defer putMap(raw) + + return decodeType(ctx, body, raw) +} diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go index 5d05c5de4..86d391a59 100644 --- a/internal/federation/dereferencing/status_permitted.go +++ b/internal/federation/dereferencing/status_permitted.go @@ -119,10 +119,10 @@ func (d *Dereferencer) isPermittedReply( ) (bool, error) { var ( - replyURI = reply.URI // Definitely set. - inReplyToURI = reply.InReplyToURI // Definitely set. - inReplyTo = reply.InReplyTo // Might not be set. - acceptIRI = reply.ApprovedByURI // Might not be set. + replyURI = reply.URI // Definitely set. + inReplyToURI = reply.InReplyToURI // Definitely set. + inReplyTo = reply.InReplyTo // Might not be set. + approvedByURI = reply.ApprovedByURI // Might not be set. ) // Check if we have a stored interaction request for parent status. @@ -165,7 +165,7 @@ func (d *Dereferencer) isPermittedReply( // If it was, and it doesn't now claim to // be approved, then we should just reject it // again, as nothing's changed since last time. - if thisRejected && acceptIRI == "" { + if thisRejected && approvedByURI == "" { // Nothing changed, // still rejected. @@ -224,16 +224,17 @@ func (d *Dereferencer) isPermittedReply( // If this reply claims to be approved, // validate this by dereferencing the - // Accept and checking the return value. + // approval and checking the return value. // No further checks are required. - if acceptIRI != "" { - return d.isPermittedByAcceptIRI( + if approvedByURI != "" { + return d.isPermittedByApprovedByIRI( ctx, + gtsmodel.InteractionReply, requestUser, reply, inReplyTo, thisReq, - acceptIRI, + approvedByURI, ) } @@ -269,7 +270,7 @@ func (d *Dereferencer) isPermittedReply( // Reply is permitted and match was *not* made // based on inclusion in a followers/following // collection. Just permit the reply full stop - // as no approval / accept URI is necessary. + // as no explicit approval is necessary. return true, nil } @@ -285,7 +286,7 @@ func (d *Dereferencer) isPermittedReply( // we can't verify the presence of a remote account // in one of another remote account's collections. // - // It's possible we'll get an Accept from the replied- + // It's possible we'll get an approval from the replied- // to account later, and we can store this reply then. return false, nil } @@ -385,30 +386,36 @@ func (d *Dereferencer) unpermittedByParent( return nil } -// isPermittedByAcceptIRI checks whether the given acceptIRI -// permits the given reply to the given inReplyTo status. -// If yes, then thisReq will be updated to reflect the -// acceptance, if it's not nil. -func (d *Dereferencer) isPermittedByAcceptIRI( +// isPermittedByApprovedByIRI checks whether the given URI +// can be dereferenced, and whether it returns either an +// Accept activity or an approval object which permits the +// given reply to the given inReplyTo status. +// +// If yes, then thisReq will be updated to +// reflect the approval, if it's not nil. +func (d *Dereferencer) isPermittedByApprovedByIRI( ctx context.Context, + interactionType gtsmodel.InteractionType, requestUser string, reply *gtsmodel.Status, inReplyTo *gtsmodel.Status, thisReq *gtsmodel.InteractionRequest, - acceptIRI string, + approvedByIRI string, ) (bool, error) { - permitted, err := d.isValidAccept( + permitted, err := d.isValidApprovedByIRI( ctx, + interactionType, requestUser, - acceptIRI, - reply.URI, - inReplyTo.AccountURI, + approvedByIRI, // approval iri + inReplyTo.AccountURI, // actor + reply.URI, // object + reply.InReplyToURI, // target ) if err != nil { // Error dereferencing means we couldn't - // get the Accept right now or it wasn't + // get the approval right now or it wasn't // valid, so we shouldn't store this status. - err := gtserror.Newf("undereferencable ApprovedByURI: %w", err) + err := gtserror.Newf("undereferencable approvedByURI: %w", err) return false, err } @@ -418,12 +425,12 @@ func (d *Dereferencer) isPermittedByAcceptIRI( return false, nil } - // Reply is permitted by this Accept. + // Reply is permitted by this approval. // If it was previously rejected or // pending approval, clear that now. reply.PendingApproval = util.Ptr(false) if thisReq != nil { - thisReq.URI = acceptIRI + thisReq.URI = approvedByIRI thisReq.AcceptedAt = time.Now() thisReq.RejectedAt = time.Time{} err := d.state.DB.UpdateInteractionRequest( @@ -576,7 +583,7 @@ func (d *Dereferencer) isPermittedBoost( // permitted but matched on a collection. // // Check if we can dereference - // an Accept that grants approval. + // an IRI that grants approval. if status.ApprovedByURI == "" { // Status doesn't claim to be approved. @@ -602,18 +609,20 @@ func (d *Dereferencer) isPermittedBoost( } // Boost claims to be approved, check - // this by dereferencing the Accept and - // inspecting the return value. - permitted, err := d.isValidAccept( + // this by dereferencing the approvedBy + // and inspecting the return value. + permitted, err := d.isValidApprovedByIRI( ctx, + gtsmodel.InteractionAnnounce, requestUser, - status.ApprovedByURI, - status.URI, - boostOf.AccountURI, + status.ApprovedByURI, // approval uri + boostOf.AccountURI, // actor + status.URI, // object + status.BoostOfURI, // target ) if err != nil { // Error dereferencing means we couldn't - // get the Accept right now or it wasn't + // get the approval right now or it wasn't // valid, so we shouldn't store this status. err := gtserror.Newf("undereferencable ApprovedByURI: %w", err) return false, err @@ -628,34 +637,36 @@ func (d *Dereferencer) isPermittedBoost( return true, nil } -// isValidAccept dereferences the activitystreams Accept at the -// specified IRI, and checks the Accept for validity against the -// provided expectedObject and expectedActor. +// isValidApprovedByIRI dereferences the activitystreams Accept or approval +// at the specified IRI, and checks the Accept or approval for validity +// against the provided expectedActor, expectedObject, and expectedTarget. // // Will return either (true, nil) if everything looked OK, an error // if something went wrong internally during deref, or (false, nil) -// if the dereferenced Accept did not meet expectations. -func (d *Dereferencer) isValidAccept( +// if the dereferenced Accept/Approval did not meet expectations. +func (d *Dereferencer) isValidApprovedByIRI( ctx context.Context, + interactionType gtsmodel.InteractionType, requestUser string, - acceptIRIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03" - expectObjectURIStr string, // Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" - expectActorURIStr string, // Eg., "https://example.org/users/someone" + approvedByIRIStr string, // approval uri Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03" + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" ) (bool, error) { l := log. WithContext(ctx). - WithField("acceptIRI", acceptIRIStr) + WithField("approvedByIRI", approvedByIRIStr) - acceptIRI, err := url.Parse(acceptIRIStr) + approvedByIRI, err := url.Parse(approvedByIRIStr) if err != nil { // Real returnable error. - err := gtserror.Newf("error parsing acceptIRI: %w", err) + err := gtserror.Newf("error parsing approvedByIRI: %w", err) return false, err } - // Don't make calls to the Accept IRI - // if it's blocked, just return false. - blocked, err := d.state.DB.IsDomainBlocked(ctx, acceptIRI.Host) + // Don't make calls to the IRI if its + // domain is blocked, just return false. + blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByIRI.Host) if err != nil { // Real returnable error. err := gtserror.Newf("error checking domain block: %w", err) @@ -663,7 +674,7 @@ func (d *Dereferencer) isValidAccept( } if blocked { - l.Info("Accept host is blocked") + l.Info("approvedByIRI host is blocked") return false, nil } @@ -674,51 +685,52 @@ func (d *Dereferencer) isValidAccept( return false, err } - // Make the call to resolve into an Acceptable. + // Make the call to the approvedByURI. // Log any error encountered here but don't // return it as it's not *our* error. - rsp, err := tsport.Dereference(ctx, acceptIRI) + rsp, err := tsport.Dereference(ctx, approvedByIRI) if err != nil { - l.Errorf("error dereferencing Accept: %v", err) + l.Errorf("error dereferencing approvedByIRI: %v", err) return false, nil } - acceptable, err := ap.ResolveAcceptable(ctx, rsp.Body) + // Try to parse response as an AP type. + t, err := ap.DecodeType(ctx, rsp.Body) // Tidy up rsp body. _ = rsp.Body.Close() if err != nil { - l.Errorf("error resolving to Accept: %v", err) + l.Errorf("error resolving to type: %v", err) return false, err } - // Extract the URI/ID of the Accept. - acceptID := ap.GetJSONLDId(acceptable) - acceptIDStr := acceptID.String() + // Extract the URI/ID of the type. + approvedByID := ap.GetJSONLDId(t) + approvedByIDStr := approvedByID.String() // Check whether input URI and final returned URI // have changed (i.e. we followed some redirects). rspURL := rsp.Request.URL rspURLStr := rspURL.String() - if rspURLStr != acceptIRIStr { - // If rspURLStr != acceptIRIStr, make sure final + if rspURLStr != approvedByIRIStr { + // If rspURLStr != approvedByIRI, make sure final // response URL is at least on the same host as // what we expected (ie., we weren't redirected // across domains), and make sure it's the same // as the ID of the Accept we were returned. switch { - case rspURL.Host != acceptIRI.Host: + case rspURL.Host != approvedByIRI.Host: l.Errorf( - "final deref host %s did not match acceptIRI host", + "final deref host %s did not match approvedByIRI host", rspURL.Host, ) return false, nil - case acceptIDStr != rspURLStr: + case approvedByIDStr != rspURLStr: l.Errorf( - "final deref uri %s did not match returned Accept ID %s", - rspURLStr, acceptIDStr, + "final deref uri %s did not match returned ID %s", + rspURLStr, approvedByIDStr, ) return false, nil } @@ -727,6 +739,52 @@ func (d *Dereferencer) isValidAccept( // Response is superficially OK, // check in more detail now. + // First try to parse type as Approval stamp. + if approvable, ok := ap.ToApprovable(t); ok { + return isValidApprovable( + ctx, + interactionType, + approvable, + approvedByID, + expectActorURIStr, // actor + expectObjectURIStr, // object + expectTargetURIStr, // target + ) + } + + // Fall back to parsing as a simple Accept. + if acceptable, ok := ap.ToAcceptable(t); ok { + return isValidAcceptable( + ctx, + acceptable, + approvedByID, + expectActorURIStr, // actor + expectObjectURIStr, // object + expectTargetURIStr, // target + ) + } + + // Type wasn't something we + // could do anything with! + l.Errorf( + "%T at %s not approvable or acceptable", + t, approvedByIRIStr, + ) + return false, nil +} + +func isValidAcceptable( + ctx context.Context, + acceptable ap.Acceptable, + acceptID *url.URL, + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" +) (bool, error) { + l := log. + WithContext(ctx). + WithField("accept", acceptID.String()) + // Extract the actor IRI and string from Accept. actorIRIs := ap.GetActorIRIs(acceptable) actorIRI, actorIRIStr := extractIRI(actorIRIs) @@ -775,6 +833,113 @@ func (d *Dereferencer) isValidAccept( return false, nil } + // If there's a Target set then verify it's + // what we expect it to be, ie., it should point + // back to the post that's being interacted with. + targetIRIs := ap.GetTargetIRIs(acceptable) + _, targetIRIStr := extractIRI(targetIRIs) + if targetIRIStr != "" && targetIRIStr != expectTargetURIStr { + l.Errorf( + "resolved Accept target IRI %s was not the same as expected target %s", + targetIRIStr, expectTargetURIStr, + ) + return false, nil + } + + // Everything looks OK. + return true, nil +} + +func isValidApprovable( + ctx context.Context, + interactionType gtsmodel.InteractionType, + approvable ap.Approvable, + approvalID *url.URL, + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" +) (bool, error) { + l := log. + WithContext(ctx). + WithField("approval", approvalID.String()) + + // Check that the type of the Approval + // matches the interaction it's approving. + switch tn := approvable.GetTypeName(); { + case (tn == ap.ObjectLikeApproval && interactionType == gtsmodel.InteractionLike), + (tn == ap.ObjectReplyApproval && interactionType == gtsmodel.InteractionReply), + (tn == ap.ObjectAnnounceApproval && interactionType == gtsmodel.InteractionAnnounce): + // All good baby! + default: + // There's a mismatch. + l.Errorf( + "approval type %s cannot approve %s", + tn, interactionType.String(), + ) + return false, nil + } + + // Extract the actor IRI and string from Approval. + actorIRIs := ap.GetAttributedTo(approvable) + actorIRI, actorIRIStr := extractIRI(actorIRIs) + switch { + case actorIRIStr == "": + l.Error("Approval missing attributedTo IRI") + return false, nil + + // Ensure the Approval actor is on + // the instance hosting the Approval. + case actorIRI.Host != approvalID.Host: + l.Errorf( + "actor %s not on the same host as Approval", + actorIRIStr, + ) + return false, nil + + // Ensure the Approval actor is who we expect + // it to be, and not someone else trying to + // do an Approval for an interaction with a + // statusable they don't own. + case actorIRIStr != expectActorURIStr: + l.Errorf( + "actor %s was not the same as expected actor %s", + actorIRIStr, expectActorURIStr, + ) + return false, nil + } + + // Extract the object IRI string from Approval. + objectIRIs := ap.GetObjectIRIs(approvable) + _, objectIRIStr := extractIRI(objectIRIs) + switch { + case objectIRIStr == "": + l.Error("missing Approval object IRI") + return false, nil + + // Ensure the Approval Object is what we expect + // it to be, ie., it's approving the interaction + // we need it to approve, and not something else. + case objectIRIStr != expectObjectURIStr: + l.Errorf( + "resolved Approval object IRI %s was not the same as expected object %s", + objectIRIStr, expectObjectURIStr, + ) + return false, nil + } + + // If there's a Target set then verify it's + // what we expect it to be, ie., it should point + // back to the post that's being interacted with. + targetIRIs := ap.GetTargetIRIs(approvable) + _, targetIRIStr := extractIRI(targetIRIs) + if targetIRIStr != "" && targetIRIStr != expectTargetURIStr { + l.Errorf( + "resolved Approval target IRI %s was not the same as expected target %s", + targetIRIStr, expectTargetURIStr, + ) + return false, nil + } + // Everything looks OK. return true, nil } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 0274fd9d7..09bbde97b 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -20,6 +20,7 @@ package federatingdb import ( "context" "errors" + "fmt" "net/url" "github.com/superseriousbusiness/activity/streams/vocab" @@ -62,8 +63,8 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return nil } - activityID := ap.GetJSONLDId(accept) - if activityID == nil { + acceptID := ap.GetJSONLDId(accept) + if acceptID == nil { // We need an ID. const text = "Accept had no id property" return gtserror.NewErrorBadRequest(errors.New(text), text) @@ -87,12 +88,11 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA // handling the ones we know how to handle. for _, object := range ap.ExtractObjects(accept) { if asType := object.GetType(); asType != nil { - // Check and handle any vocab.Type objects. - switch name := asType.GetTypeName(); name { + switch name := asType.GetTypeName(); { // ACCEPT FOLLOW - case ap.ActivityFollow: + case name == ap.ActivityFollow: if err := f.acceptFollowType( ctx, asType, @@ -102,6 +102,50 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return err } + // ACCEPT TYPE-HINTED LIKE + // + // ie., a Like with just `id` + // and `type` properties set. + case name == ap.ActivityLike: + objIRI := ap.GetJSONLDId(asType) + if objIRI == nil { + log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name) + continue + } + + if err := f.acceptLikeIRI( + ctx, + acceptID, + accept, + objIRI.String(), + receivingAcct, + requestingAcct, + ); err != nil { + return err + } + + // ACCEPT TYPE-HINTED REPLY OR ANNOUNCE. + // + // ie., a statusable or Announce with + // just `id` and `type` properties set. + case name == ap.ActivityAnnounce || ap.IsStatusable(name): + objIRI := ap.GetJSONLDId(asType) + if objIRI == nil { + log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name) + continue + } + + if err := f.acceptOtherIRI( + ctx, + acceptID, + accept, + objIRI, + receivingAcct, + requestingAcct, + ); err != nil { + return err + } + // UNHANDLED default: log.Debugf(ctx, "unhandled object type: %s", name) @@ -127,7 +171,8 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA case uris.IsLikePath(objIRI): if err := f.acceptLikeIRI( ctx, - activityID.String(), + acceptID, + accept, objIRI.String(), receivingAcct, requestingAcct, @@ -135,14 +180,15 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return err } - // ACCEPT OTHER (reply? boost?) + // ACCEPT OTHER (reply? announce?) // // Don't check on IsStatusesPath // as this may be a remote status. default: if err := f.acceptOtherIRI( ctx, - activityID, + acceptID, + accept, objIRI, receivingAcct, requestingAcct, @@ -292,7 +338,8 @@ func (f *federatingDB) acceptFollowIRI( func (f *federatingDB) acceptOtherIRI( ctx context.Context, - activityID *url.URL, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, objectIRI *url.URL, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -309,7 +356,8 @@ func (f *federatingDB) acceptOtherIRI( // objectIRI, proceed to accept it. return f.acceptStoredStatus( ctx, - activityID, + acceptID, + accept, status, receivingAcct, requestingAcct, @@ -348,13 +396,21 @@ func (f *federatingDB) acceptOtherIRI( // This may be a reply, or it may be a boost, // we can't know yet without dereferencing it, // but let the processor worry about that. + // + // TODO: do something with type hinting here. apObjectType := ap.ObjectUnknown + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + // Pass to the processor and let them handle side effects. f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{ APObjectType: apObjectType, APActivityType: ap.ActivityAccept, - APIRI: activityID, + APIRI: approvedByURI, APObject: objectIRI, Receiving: receivingAcct, Requesting: requestingAcct, @@ -365,7 +421,8 @@ func (f *federatingDB) acceptOtherIRI( func (f *federatingDB) acceptStoredStatus( ctx context.Context, - activityID *url.URL, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, status *gtsmodel.Status, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -391,9 +448,15 @@ func (f *federatingDB) acceptStoredStatus( return gtserror.NewErrorForbidden(errors.New(text), text) } - // Mark the status as approved by this Accept URI. + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + + // Mark the status as approved by this URI. status.PendingApproval = util.Ptr(false) - status.ApprovedByURI = activityID.String() + status.ApprovedByURI = approvedByURI.String() if err := f.state.DB.UpdateStatus( ctx, status, @@ -428,7 +491,8 @@ func (f *federatingDB) acceptStoredStatus( func (f *federatingDB) acceptLikeIRI( ctx context.Context, - activityID string, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, objectIRI string, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -482,9 +546,15 @@ func (f *federatingDB) acceptLikeIRI( return gtserror.NewErrorForbidden(errors.New(text), text) } - // Mark the fave as approved by this Accept URI. + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + + // Mark the fave as approved by this URI. fave.PendingApproval = util.Ptr(false) - fave.ApprovedByURI = activityID + fave.ApprovedByURI = approvedByURI.String() if err := f.state.DB.UpdateStatusFave( ctx, fave, @@ -507,3 +577,72 @@ func (f *federatingDB) acceptLikeIRI( return nil } + +// approvedByURI extracts the appropriate *url.URL +// to use as an interaction's approvedBy value by +// checking to see if the Accept has a result URL set. +// If that result URL exists, is an IRI (not a type), +// and is on the same host as the Accept ID, then the +// result URI will be returned. In all other cases, +// the Accept ID is returned unchanged. +// +// Error is only returned if the result URI is set +// but the host differs from the Accept ID host. +// +// TODO: This function should be updated at some point +// to check for inlined result type, and see if type is +// a LikeApproval, ReplyApproval, or AnnounceApproval, +// and check the attributedTo, object, and target of +// the approval as well. But this'll do for now. +func approvedByURI( + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, +) (*url.URL, error) { + // Check if the Accept has a `result` property + // set on it (which should be an approval). + resultProp := accept.GetActivityStreamsResult() + if resultProp == nil { + // No result, + // use AcceptID. + return acceptID, nil + } + + if resultProp.Len() != 1 { + // Result was unexpected + // length, can't use this. + return acceptID, nil + } + + result := resultProp.At(0) + if result == nil { + // Result entry + // was nil, huh! + return acceptID, nil + } + + if !result.IsIRI() { + // Can't handle + // inlined yet. + return acceptID, nil + } + + resultIRI := result.GetIRI() + if resultIRI == nil { + // Result entry + // was nil, huh! + return acceptID, nil + } + + if resultIRI.Host != acceptID.Host { + // What the boobs is this? + err := fmt.Errorf( + "host of result %s differed from host of Accept %s", + resultIRI, accept, + ) + return nil, err + } + + // Use the result IRI we've been + // given instead of the acceptID. + return resultIRI, nil +} diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index cf93a5ec5..ce5b8b5d1 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -844,16 +844,16 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed return gtserror.Newf("%T not parseable as *url.URL", fMsg.APObject) } - acceptIRI := fMsg.APIRI - if acceptIRI == nil { - return gtserror.New("acceptIRI was nil") + approvedByURI := fMsg.APIRI + if approvedByURI == nil { + return gtserror.New("approvedByURI was nil") } // Assume we're accepting a status; create a // barebones status for dereferencing purposes. bareStatus := >smodel.Status{ URI: objectIRI.String(), - ApprovedByURI: acceptIRI.String(), + ApprovedByURI: approvedByURI.String(), } // Call RefreshStatus() to process the provided @@ -872,7 +872,7 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed } // No error means it was indeed a remote status, and the - // given acceptIRI permitted it. Timeline and notify it. + // given approvedByURI permitted it. Timeline and notify it. if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil { log.Errorf(ctx, "error timelining and notifying status: %v", err) } |