[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
This commit is contained in:
tobi 2025-02-19 18:09:54 +01:00 committed by GitHub
commit 96716e4f43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 20960 additions and 2964 deletions

View file

@ -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).

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 := &gtsmodel.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)
}