summaryrefslogtreecommitdiff
path: root/internal/federation
diff options
context:
space:
mode:
Diffstat (limited to 'internal/federation')
-rw-r--r--internal/federation/dereferencing/status_permitted.go291
-rw-r--r--internal/federation/federatingdb/accept.go173
2 files changed, 384 insertions, 80 deletions
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
+}