diff options
Diffstat (limited to 'internal/federation/federatingdb/accept.go')
| -rw-r--r-- | internal/federation/federatingdb/accept.go | 369 |
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 |
