summaryrefslogtreecommitdiff
path: root/internal/federation/federatingdb/accept.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/federation/federatingdb/accept.go')
-rw-r--r--internal/federation/federatingdb/accept.go369
1 files changed, 350 insertions, 19 deletions
diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go
index 2e4948a0e..d7606c4fa 100644
--- a/internal/federation/federatingdb/accept.go
+++ b/internal/federation/federatingdb/accept.go
@@ -22,6 +22,7 @@ import (
"errors"
"fmt"
"net/url"
+ "time"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/ap"
@@ -39,7 +40,7 @@ func (f *DB) GetAccept(
ctx context.Context,
acceptIRI *url.URL,
) (vocab.ActivityStreamsAccept, error) {
- approval, err := f.state.DB.GetInteractionRequestByURI(ctx, acceptIRI.String())
+ approval, err := f.state.DB.GetInteractionRequestByResponseURI(ctx, acceptIRI.String())
if err != nil {
return nil, err
}
@@ -63,9 +64,9 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
return nil
}
+ // Ensure an activity ID is given.
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)
}
@@ -109,7 +110,7 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
case name == ap.ActivityLike:
objIRI := ap.GetJSONLDId(asType)
if objIRI == nil {
- log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name)
+ log.Warnf(ctx, "missing id for inlined object %s: %s", name, acceptID)
continue
}
@@ -131,7 +132,7 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
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)
+ log.Warnf(ctx, "missing id for inlined object %s: %s", name, acceptID)
continue
}
@@ -146,9 +147,38 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
return err
}
+ // Todo: ACCEPT POLITE INLINED LIKE REQUEST.
+ //
+ // Implement this when we start
+ // sending out polite LikeRequests.
+
+ // ACCEPT POLITE INLINED REPLY REQUEST
+ case name == ap.ActivityReplyRequest:
+ replyReq, ok := asType.(vocab.GoToSocialReplyRequest)
+ if !ok {
+ const text = "malformed ReplyRequest as object of Accept"
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ if err := f.acceptPoliteReplyRequest(
+ ctx,
+ acceptID,
+ accept,
+ replyReq,
+ receivingAcct,
+ requestingAcct,
+ ); err != nil {
+ return err
+ }
+
+ // Todo: ACCEPT POLITE INLINED ANNOUNCE REQUEST
+ //
+ // Implement this when we start
+ // sending out polite AnnounceRequests.
+
// UNHANDLED
default:
- log.Debugf(ctx, "unhandled object type: %s", name)
+ log.Debugf(ctx, "unhandled object type %s: %s", name, acceptID)
}
} else if object.IsIRI() {
@@ -445,8 +475,7 @@ func (f *DB) acceptStoredStatus(
// Mark the status as approved by this URI.
status.PendingApproval = util.Ptr(false)
status.ApprovedByURI = approvedByURI.String()
- if err := f.state.DB.UpdateStatus(
- ctx,
+ if err := f.state.DB.UpdateStatus(ctx,
status,
"pending_approval",
"approved_by_uri",
@@ -543,8 +572,7 @@ func (f *DB) acceptLikeIRI(
// Mark the fave as approved by this URI.
fave.PendingApproval = util.Ptr(false)
fave.ApprovedByURI = approvedByURI.String()
- if err := f.state.DB.UpdateStatusFave(
- ctx,
+ if err := f.state.DB.UpdateStatusFave(ctx,
fave,
"pending_approval",
"approved_by_uri",
@@ -566,6 +594,316 @@ func (f *DB) acceptLikeIRI(
return nil
}
+// partialAcceptInteractionRequest represents a
+// partially-parsed accept of an interaction request
+// returned from parseAcceptInteractionRequestable.
+type partialAcceptInteractionRequest struct {
+ intReqURI *url.URL
+ actorURI *url.URL
+ parentURI *url.URL
+ instrumentURI *url.URL
+ authURI *url.URL
+ intReq *gtsmodel.InteractionRequest // May be nil.
+}
+
+// parseAcceptInteractionRequestable does some initial parsing
+// and validation of the given Accept with inlined polite
+// interaction request (LikeRequest, ReplyRequest, AnnounceRequest).
+//
+// Will return nil, nil if there's no need for further processing.
+func (f *DB) parseAcceptInteractionRequestable(
+ ctx context.Context,
+ accept vocab.ActivityStreamsAccept,
+ intRequestable ap.InteractionRequestable,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) (*partialAcceptInteractionRequest, error) {
+ intReqURI := ap.GetJSONLDId(intRequestable)
+ if intReqURI == nil {
+ const text = "no id set on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ // Ensure we have actor IRI on
+ // the interaction requestable.
+ actors := ap.GetActorIRIs(intRequestable)
+ if len(actors) != 1 {
+ const text = "invalid or missing actor property on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+ actorURI := actors[0]
+
+ // Ensure we have an object URI, which
+ // should point to the statusable being
+ // interacted with, ie., the parent status.
+ objects := ap.GetObjectIRIs(intRequestable)
+ if len(objects) != 1 {
+ const text = "invalid or missing object property on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+ parentURI := objects[0]
+
+ // Ensure we have instrument, which should
+ // be or point to the activity/object that
+ // interacts with the parent status.
+ instruments := ap.ExtractInstruments(intRequestable)
+ if len(instruments) != 1 {
+ const text = "invalid or missing instrument property on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+ instrument := instruments[0]
+
+ // We just need the URI for the instrument,
+ // not the whole type, which we can either
+ // fetch from remote or get locally.
+ var instrumentURI *url.URL
+ if instrument.IsIRI() {
+ instrumentURI = instrument.GetIRI()
+ } else {
+ t := instrument.GetType()
+ if t == nil {
+ const text = "nil instrument type on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+ instrumentURI = ap.GetJSONLDId(t)
+ }
+
+ // Ensure we have result URI, which should
+ // point to an authorization for this interaction.
+ results := ap.GetResultIRIs(accept)
+ if len(results) != 1 {
+ const text = "invalid or missing result property on embedded interaction request"
+ return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+ authURI := results[0]
+
+ // Check if we have a gtsmodel interaction
+ // request already stored for this interaction.
+ intReq, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, instrumentURI.String())
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ // Real db error.
+ return nil, gtserror.Newf("db error getting interaction request: %w", err)
+ }
+
+ if intReq == nil {
+
+ // No request stored for this interaction.
+ // Means this is *probably* a remote interaction
+ // with a remote status. Double check this.
+ host := config.GetHost()
+ acctDomain := config.GetAccountDomain()
+ if instrumentURI.Host == host ||
+ instrumentURI.Host == acctDomain ||
+ intReqURI.Host == host ||
+ intReqURI.Host == acctDomain {
+ // Claims to be Accepting something of ours,
+ // but we don't have an interaction request
+ // stored. Most likely it's been deleted in
+ // the meantime, or this is a mistake. Bail.
+ return nil, nil
+ }
+
+ // This must be an Accept of a remote interaction
+ // request. Ensure relevance of this message by
+ // checking that receiver follows requester.
+ following, err := f.state.DB.IsFollowing(
+ ctx,
+ receivingAcct.ID,
+ requestingAcct.ID,
+ )
+ if err != nil {
+ err := gtserror.Newf("db error checking following: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if !following {
+ // If we don't follow this person, and
+ // they're not Accepting something we
+ // created, then we don't care.
+ return nil, nil
+ }
+
+ } else {
+
+ // Request stored for this interaction URI.
+ //
+ // Note: this path is not actually possible until v0.21.0,
+ // because we don't send out polite requests yet in v0.20.0.
+
+ // If the request is already accepted,
+ // we don't need to do anything at all.
+ if intReq.IsAccepted() {
+ return nil, nil
+ }
+
+ // The person doing the Accept must be the
+ // same as the target of the interaction request.
+ if intReq.TargetAccountID != requestingAcct.ID {
+ const text = "cannot Accept interaction request on another actor's behalf"
+ return nil, gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // The stored interaction request and the inlined
+ // interaction request must have the same target status.
+ if intReq.TargetStatus.URI != parentURI.String() {
+ const text = "Accept interaction request mismatched object URI"
+ return nil, gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // The stored interaction request and the inlined
+ // interaction request must have the same URI.
+ if intReq.InteractionRequestURI != intReqURI.String() {
+ const text = "Accept interaction request mismatched id"
+ return nil, gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+ }
+
+ // Return the things.
+ return &partialAcceptInteractionRequest{
+ intReqURI: intReqURI,
+ actorURI: actorURI,
+ parentURI: parentURI,
+ instrumentURI: instrumentURI,
+ authURI: authURI,
+ intReq: intReq, // May be nil.
+ }, nil
+}
+
+// acceptPoliteReplyRequest handles the Accept of a polite ReplyRequest,
+// ie., something that looks like this:
+//
+// {
+// "@context": [
+// "https://www.w3.org/ns/activitystreams",
+// "https://gotosocial.org/ns"
+// ],
+// "type": "Accept",
+// "to": "https://example.com/users/bob",
+// "id": "https://example.com/users/alice/activities/1234",
+// "actor": "https://example.com/users/alice",
+// "object": {
+// "type": "ReplyRequest",
+// "id": "https://example.com/users/bob/interaction_requests/12345",
+// "actor": "https://example.com/users/bob",
+// "object": "https://example.com/users/alice/statuses/1",
+// "instrument": "https://example.org/users/bob/statuses/12345"
+// },
+// "result": "https://example.com/users/alice/authorizations/1"
+// }
+func (f *DB) acceptPoliteReplyRequest(
+ ctx context.Context,
+ acceptID *url.URL,
+ accept vocab.ActivityStreamsAccept,
+ replyRequest vocab.GoToSocialReplyRequest,
+ receivingAcct *gtsmodel.Account,
+ requestingAcct *gtsmodel.Account,
+) error {
+ // Parse out the Accept and
+ // embedded interaction requestable.
+ partial, err := f.parseAcceptInteractionRequestable(
+ ctx,
+ accept,
+ replyRequest,
+ receivingAcct,
+ requestingAcct,
+ )
+ if err != nil {
+ return err
+ }
+
+ if partial == nil {
+ // Nothing to do!
+ return nil
+ }
+
+ if partial.intReq == nil {
+ // This is a remote accept of a remote reply.
+ //
+ // Process dereferencing etc asynchronously, leaving
+ // the interaction request as nil. We don't need to
+ // create an int req for remote accepts of remote
+ // replies, we can just validate + store the auth URI.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityReplyRequest,
+ APActivityType: ap.ActivityAccept,
+ APIRI: partial.authURI,
+ APObject: partial.instrumentURI,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
+ return nil
+ }
+
+ // We already have a request stored for this interaction.
+ //
+ // Note: this path is not actually possible until v0.21.0,
+ // because we don't send out polite requests yet in v0.20.0.
+
+ // Make sure the stored interaction request
+ // lines up with the Accept ReplyRequest.
+ if partial.intReq.InteractionType != gtsmodel.InteractionReply {
+ const text = "Accept ReplyRequest targets interaction request that isn't of type Reply"
+ return gtserror.NewErrorBadRequest(errors.New(text), text)
+ }
+
+ // The stored reply must be the same as
+ // the instrument of the ReplyRequest.
+ reply := partial.intReq.Reply
+ if reply.URI != partial.instrumentURI.String() {
+ const text = "Accept ReplyRequest mismatched instrument URI"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // The actor of the stored reply must be the
+ // same as the actor of the ReplyRequest.
+ if reply.AccountURI != partial.actorURI.String() {
+ const text = "Accept ReplyRequest mismatched actor URI"
+ return gtserror.NewErrorForbidden(errors.New(text), text)
+ }
+
+ // This all looks good, we can update the
+ // interaction request and stored reply.
+ unlock := f.state.FedLocks.Lock(partial.intReq.InteractionURI)
+ defer unlock()
+
+ authURIStr := partial.authURI.String()
+ partial.intReq.AcceptedAt = time.Now()
+ partial.intReq.AuthorizationURI = authURIStr
+ partial.intReq.ResponseURI = acceptID.String()
+ if err := f.state.DB.UpdateInteractionRequest(
+ ctx, partial.intReq,
+ "accepted_at",
+ "authorization_uri",
+ "response_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating interaction request: %w", err)
+ }
+
+ reply.ApprovedByURI = authURIStr
+ reply.PendingApproval = util.Ptr(false)
+ if err := f.state.DB.UpdateStatus(
+ ctx, reply,
+ "approved_by_uri",
+ "pending_approval",
+ ); err != nil {
+ return gtserror.Newf("db error updating status: %w", err)
+ }
+
+ // Handle any remaining side effects in the processor.
+ f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
+ APObjectType: ap.ActivityReplyRequest,
+ APActivityType: ap.ActivityAccept,
+ APIRI: partial.authURI,
+ APObject: partial.instrumentURI,
+ GTSModel: partial.intReq,
+ Receiving: receivingAcct,
+ Requesting: requestingAcct,
+ })
+
+ 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.
@@ -577,11 +915,8 @@ func (f *DB) acceptLikeIRI(
// 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.
+// TODO: This function could be updated at some
+// point to check for inlined result type.
func approvedByURI(
acceptID *url.URL,
accept vocab.ActivityStreamsAccept,
@@ -623,11 +958,7 @@ func approvedByURI(
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
+ return nil, fmt.Errorf("host of result %s differed from host of Accept %s", resultIRI, accept)
}
// Use the result IRI we've been