mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-01 22:22:24 -05:00
[feature] Support new model of interaction flow for forward compat with v0.21.0 (#4394)
~~Still WIP!~~ This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0. Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them. As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type. Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation). Migrations tested on both Postgres and SQLite. Co-authored-by: kim <grufwub@gmail.com> Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
parent
33fed81a8d
commit
754b7be9cf
126 changed files with 6637 additions and 1778 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue