[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:
tobi 2025-09-14 15:37:35 +02:00 committed by tobi
commit 754b7be9cf
126 changed files with 6637 additions and 1778 deletions

View file

@ -82,10 +82,7 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode
// Neither follows nor follow requests, so
// create and store a new follow request.
followID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
followID := id.NewRandomULID()
followURI := uris.GenerateURIForFollow(requestingAccount.Username, followID)
fr := &gtsmodel.FollowRequest{

View file

@ -64,17 +64,12 @@ func (p *Processor) RuleGet(ctx context.Context, id string) (*apimodel.AdminInst
// RuleCreate adds a new rule to the instance.
func (p *Processor) RuleCreate(ctx context.Context, form *apimodel.InstanceRuleCreateRequest) (*apimodel.AdminInstanceRule, gtserror.WithCode) {
ruleID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new instance rule: %s", err), "error creating rule ID")
}
rule := &gtsmodel.Rule{
ID: ruleID,
ID: id.NewRandomULID(),
Text: form.Text,
}
if err = p.state.DB.PutRule(ctx, rule); err != nil {
if err := p.state.DB.PutRule(ctx, rule); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}

View file

@ -86,10 +86,7 @@ func (p *Processor) Create(
}
// Generate random client ID.
clientID, err := id.NewRandomULID()
if err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
clientID := id.NewRandomULID()
// Generate + store app
// to put in the database.

View file

@ -19,10 +19,8 @@ package fedi
import (
"context"
"errors"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
@ -34,44 +32,18 @@ import (
func (p *Processor) AcceptGet(
ctx context.Context,
requestedUser string,
reqID string,
) (interface{}, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
intReqID string,
) (any, gtserror.WithCode) {
// Ensure valid request, intReq exists, etc.
intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID)
if errWithCode != nil {
return nil, errWithCode
}
if auth.handshakingURI != nil {
// We're currently handshaking, which means
// we don't know this account yet. This should
// be a very rare race condition.
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
return nil, gtserror.NewErrorInternalError(err)
}
receivingAcct := auth.receivingAcct
req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting interaction request %s: %w", reqID, err)
return nil, gtserror.NewErrorInternalError(err)
}
if req == nil || !req.IsAccepted() {
// Request doesn't exist or hasn't been accepted.
err := gtserror.Newf("interaction request %s not found", reqID)
return nil, gtserror.NewErrorNotFound(err)
}
if req.TargetAccountID != receivingAcct.ID {
const text = "interaction request does not belong to receiving account"
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
accept, err := p.converter.InteractionReqToASAccept(ctx, req)
// Convert + serialize the Accept.
accept, err := p.converter.InteractionReqToASAccept(ctx, intReq)
if err != nil {
err := gtserror.Newf("error converting accept: %w", err)
err := gtserror.Newf("error converting to accept: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}

View file

@ -0,0 +1,57 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package fedi
import (
"context"
"code.superseriousbusiness.org/gotosocial/internal/ap"
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
)
// AuthorizationGet handles the getting of a fedi/activitypub
// representation of a local interaction authorization.
//
// It performs appropriate authentication before
// returning a JSON serializable interface.
func (p *Processor) AuthorizationGet(
ctx context.Context,
requestedUser string,
intReqID string,
) (any, gtserror.WithCode) {
// Ensure valid request, intReq exists, etc.
intReq, errWithCode := p.validateIntReqRequest(ctx, requestedUser, intReqID)
if errWithCode != nil {
return nil, errWithCode
}
// Convert + serialize the Authorization.
authorization, err := p.converter.InteractionReqToASAuthorization(ctx, intReq)
if err != nil {
err := gtserror.Newf("error converting to authorization: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
data, err := ap.Serialize(authorization)
if err != nil {
err := gtserror.Newf("error serializing accept: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
return data, nil
}

View file

@ -20,6 +20,7 @@ package fedi
import (
"context"
"errors"
"fmt"
"net/url"
"code.superseriousbusiness.org/gotosocial/internal/db"
@ -81,3 +82,52 @@ func (p *Processor) authenticate(ctx context.Context, requestedUser string) (*co
receivingAcct: receiver,
}, nil
}
// validateIntReqRequest is a shortcut function
// for returning an accepted interaction request
// targeting `requestedUser`.
func (p *Processor) validateIntReqRequest(
ctx context.Context,
requestedUser string,
intReqID string,
) (*gtsmodel.InteractionRequest, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
if errWithCode != nil {
return nil, errWithCode
}
if auth.handshakingURI != nil {
// We're currently handshaking, which means we don't know
// this account yet. This should be a very rare race condition.
err := gtserror.Newf("network race handshaking %s", auth.handshakingURI)
return nil, gtserror.NewErrorInternalError(err)
}
// Fetch interaction request with the given ID.
req, err := p.state.DB.GetInteractionRequestByID(ctx, intReqID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err := gtserror.Newf("db error getting interaction request %s: %w", intReqID, err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure that this is an existing
// and *accepted* interaction request.
if req == nil || !req.IsAccepted() {
const text = "interaction request not found"
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
// Ensure interaction request was accepted
// by the account in the request path.
if req.TargetAccountID != auth.receivingAcct.ID {
text := fmt.Sprintf(
"account %s is not targeted by interaction request %s and therefore can't accept it",
requestedUser, intReqID,
)
return nil, gtserror.NewErrorNotFound(errors.New(text))
}
// All fine.
return req, nil
}

View file

@ -65,14 +65,16 @@ func (p *Processor) Accept(
defer unlock()
// Mark the request as accepted
// and generate a URI for it.
// and generate URIs for it.
req.AcceptedAt = time.Now()
req.URI = uris.GenerateURIForAccept(acct.Username, req.ID)
req.ResponseURI = uris.GenerateURIForAccept(acct.Username, req.ID)
req.AuthorizationURI = uris.GenerateURIForAuthorization(acct.Username, req.ID)
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"accepted_at",
"uri",
"response_uri",
"authorization_uri",
); err != nil {
err := gtserror.Newf("db error updating interaction request: %w", err)
return nil, gtserror.NewErrorInternalError(err)
@ -132,7 +134,7 @@ func (p *Processor) acceptLike(
// Update the Like.
req.Like.PendingApproval = util.Ptr(false)
req.Like.PreApproved = false
req.Like.ApprovedByURI = req.URI
req.Like.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
req.Like,
@ -173,7 +175,7 @@ func (p *Processor) acceptReply(
// Update the Reply.
req.Reply.PendingApproval = util.Ptr(false)
req.Reply.PreApproved = false
req.Reply.ApprovedByURI = req.URI
req.Reply.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
req.Reply,
@ -214,7 +216,7 @@ func (p *Processor) acceptAnnounce(
// Update the Announce.
req.Announce.PendingApproval = util.Ptr(false)
req.Announce.PreApproved = false
req.Announce.ApprovedByURI = req.URI
req.Announce.ApprovedByURI = req.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
req.Announce,

View file

@ -67,7 +67,7 @@ func (suite *AcceptTestSuite) TestAccept() {
suite.FailNow(err.Error())
}
suite.False(*dbStatus.PendingApproval)
suite.Equal(dbReq.URI, dbStatus.ApprovedByURI)
suite.Equal(dbReq.AuthorizationURI, dbStatus.ApprovedByURI)
// Wait for a notification
// for interacting status.

View file

@ -66,12 +66,12 @@ func (p *Processor) Reject(
// Mark the request as rejected
// and generate a URI for it.
req.RejectedAt = time.Now()
req.URI = uris.GenerateURIForReject(acct.Username, req.ID)
req.ResponseURI = uris.GenerateURIForReject(acct.Username, req.ID)
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"rejected_at",
"uri",
"response_uri",
); err != nil {
err := gtserror.Newf("db error updating interaction request: %w", err)
return nil, gtserror.NewErrorInternalError(err)

View file

@ -524,7 +524,7 @@ func (p *Processor) byURI(
switch {
case gtserror.IsUnretrievable(err),
gtserror.IsWrongType(err),
gtserror.NotPermitted(err):
gtserror.IsNotPermitted(err):
log.Debugf(ctx,
"semi-expected error type looking up %s as status: %v",
uri, err,

View file

@ -115,7 +115,7 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
// Address the delete CC public.
deleteCC := streams.NewActivityStreamsCcProperty()
deleteCC.AppendIRI(ap.PublicURI())
deleteCC.AppendIRI(ap.PublicIRI())
delete.SetActivityStreamsCc(deleteCC)
// Send the Delete via the Actor's outbox.
@ -491,12 +491,7 @@ func (f *federate) UndoAnnounce(ctx context.Context, boost *gtsmodel.Status) err
}
// Recreate the ActivityStreams Announce.
asAnnounce, err := f.converter.BoostToAS(
ctx,
boost,
boost.Account,
boost.BoostOfAccount,
)
asAnnounce, err := f.converter.BoostToAS(ctx, boost)
if err != nil {
return gtserror.Newf("error converting boost to AS: %w", err)
}
@ -767,12 +762,7 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
}
// Create the ActivityStreams Announce.
announce, err := f.converter.BoostToAS(
ctx,
boost,
boost.Account,
boost.BoostOfAccount,
)
announce, err := f.converter.BoostToAS(ctx, boost)
if err != nil {
return gtserror.Newf("error converting boost to AS: %w", err)
}
@ -1104,7 +1094,7 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
ap.AppendTo(move, followersIRI)
// Address the move CC public.
ap.AppendCc(move, ap.PublicURI())
ap.AppendCc(move, ap.PublicIRI())
// Send the Move via the Actor's outbox.
if _, err := f.FederatingActor().Send(

View file

@ -287,7 +287,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
if err := p.utils.requestReply(ctx, status); err != nil {
if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
return gtserror.Newf("error pending reply: %w", err)
}
@ -310,19 +310,22 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// URI attached.
// Store an already-accepted interaction request.
id := id.NewULID()
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: status.InReplyToID,
TargetAccountID: status.InReplyToAccountID,
TargetAccount: status.InReplyToAccount,
InteractingAccountID: status.AccountID,
InteractingAccount: status.Account,
InteractionURI: status.URI,
InteractionType: gtsmodel.InteractionReply,
Reply: status,
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: status.InReplyToID,
TargetAccountID: status.InReplyToAccountID,
TargetAccount: status.InReplyToAccount,
InteractingAccountID: status.AccountID,
InteractingAccount: status.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
InteractionURI: status.URI,
InteractionType: gtsmodel.InteractionReply,
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
Reply: status,
ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -331,7 +334,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// Mark the status as now approved.
status.PendingApproval = util.Ptr(false)
status.PreApproved = false
status.ApprovedByURI = approval.URI
status.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
status,
@ -494,7 +497,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
if err := p.utils.requestFave(ctx, fave); err != nil {
if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
return gtserror.Newf("error pending fave: %w", err)
}
@ -517,19 +520,22 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// URI attached.
// Store an already-accepted interaction request.
id := id.NewULID()
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: fave.StatusID,
TargetAccountID: fave.TargetAccountID,
TargetAccount: fave.TargetAccount,
InteractingAccountID: fave.AccountID,
InteractingAccount: fave.Account,
InteractionURI: fave.URI,
InteractionType: gtsmodel.InteractionLike,
Like: fave,
URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: fave.StatusID,
TargetAccountID: fave.TargetAccountID,
TargetAccount: fave.TargetAccount,
InteractingAccountID: fave.AccountID,
InteractingAccount: fave.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
InteractionURI: fave.URI,
InteractionType: gtsmodel.InteractionLike,
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
Like: fave,
ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -538,7 +544,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// Mark the fave itself as now approved.
fave.PendingApproval = util.Ptr(false)
fave.PreApproved = false
fave.ApprovedByURI = approval.URI
fave.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
fave,
@ -589,7 +595,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
if err := p.utils.requestAnnounce(ctx, boost); err != nil {
if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
return gtserror.Newf("error pending boost: %w", err)
}
@ -612,19 +618,22 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// URI attached.
// Store an already-accepted interaction request.
id := id.NewULID()
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: boost.BoostOfID,
TargetAccountID: boost.BoostOfAccountID,
TargetAccount: boost.BoostOfAccount,
InteractingAccountID: boost.AccountID,
InteractingAccount: boost.Account,
InteractionURI: boost.URI,
InteractionType: gtsmodel.InteractionAnnounce,
Announce: boost,
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: boost.BoostOfID,
TargetAccountID: boost.BoostOfAccountID,
TargetAccount: boost.BoostOfAccount,
InteractingAccountID: boost.AccountID,
InteractingAccount: boost.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
InteractionURI: boost.URI,
InteractionType: gtsmodel.InteractionAnnounce,
Polite: util.Ptr(false), // TODO: Change this in v0.21.0 when we only send out polite requests.
Announce: boost,
ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -633,7 +642,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// Mark the boost itself as now approved.
boost.PendingApproval = util.Ptr(false)
boost.PreApproved = false
boost.ApprovedByURI = approval.URI
boost.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
boost,

View file

@ -88,6 +88,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.CreateStatus(ctx, fMsg)
// REQUEST TO REPLY TO A STATUS
case ap.ActivityReplyRequest:
return p.fediAPI.CreateReplyRequest(ctx, fMsg)
// CREATE FOLLOW (request)
case ap.ActivityFollow:
return p.fediAPI.CreateFollowReq(ctx, fMsg)
@ -96,10 +100,18 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ActivityLike:
return p.fediAPI.CreateLike(ctx, fMsg)
// REQUEST TO LIKE A STATUS
case ap.ActivityLikeRequest:
return p.fediAPI.CreateLikeRequest(ctx, fMsg)
// CREATE ANNOUNCE/BOOST
case ap.ActivityAnnounce:
return p.fediAPI.CreateAnnounce(ctx, fMsg)
// REQUEST TO BOOST A STATUS
case ap.ActivityAnnounceRequest:
return p.fediAPI.CreateAnnounceRequest(ctx, fMsg)
// CREATE BLOCK
case ap.ActivityBlock:
return p.fediAPI.CreateBlock(ctx, fMsg)
@ -146,11 +158,15 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.AcceptReply(ctx, fMsg)
// ACCEPT (pending) POLITE REPLY REQUEST
case ap.ActivityReplyRequest:
return p.fediAPI.AcceptPoliteReplyRequest(ctx, fMsg)
// ACCEPT (pending) ANNOUNCE
case ap.ActivityAnnounce:
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
// ACCEPT (remote) REPLY or ANNOUNCE
// ACCEPT (remote) IMPOLITE REPLY or ANNOUNCE
case ap.ObjectUnknown:
return p.fediAPI.AcceptRemoteStatus(ctx, fMsg)
}
@ -219,6 +235,9 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
return gtserror.Newf("unhandled: %s %s", fMsg.APActivityType, fMsg.APObjectType)
}
// CreateStatus handles the creation of a status/post sent as a Create message.
// It is also capable of handling impolite reply requests to local + remote statuses,
// ie., replies sent directly without doing the ReplyRequest process first.
func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
var (
status *gtsmodel.Status
@ -291,7 +310,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
if err := p.utils.requestReply(ctx, status); err != nil {
if err := p.utils.impoliteReplyRequest(ctx, status); err != nil {
return gtserror.Newf("error pending reply: %w", err)
}
@ -306,20 +325,24 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// collection. Do the Accept immediately and
// then process everything else as normal.
// Store an already-accepted interaction request.
id := id.NewULID()
// Store an already-accepted
// impolite interaction request.
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: status.InReplyToID,
TargetAccountID: status.InReplyToAccountID,
TargetAccount: status.InReplyToAccount,
InteractingAccountID: status.AccountID,
InteractingAccount: status.Account,
InteractionURI: status.URI,
InteractionType: gtsmodel.InteractionReply,
Reply: status,
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: status.InReplyToID,
TargetAccountID: status.InReplyToAccountID,
TargetAccount: status.InReplyToAccount,
InteractingAccountID: status.AccountID,
InteractingAccount: status.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix),
InteractionURI: status.URI,
InteractionType: gtsmodel.InteractionReply,
Polite: util.Ptr(false),
Reply: status,
ResponseURI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(status.InReplyToAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -328,7 +351,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// Mark the status as now approved.
status.PendingApproval = util.Ptr(false)
status.PreApproved = false
status.ApprovedByURI = approval.URI
status.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
status,
@ -365,6 +388,118 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
return nil
}
// CreateReplyRequest handles a polite ReplyRequest.
// This is distinct from CreateStatus, which is capable
// of handling both "normal" top-level status creation,
// in addition to *impolite* reply requests.
func (p *fediAPI) CreateReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Extract the ap model Statusable
// set by the federating db.
statusable, ok := fMsg.APObject.(ap.Statusable)
if !ok {
return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObject)
}
// Call RefreshStatus to parse and process the
// statusable. This will also check permissions.
replyURI := ap.GetJSONLDId(statusable).String()
reply, _, err := p.federate.RefreshStatus(ctx,
fMsg.Receiving.Username,
&gtsmodel.Status{
URI: replyURI,
Local: util.Ptr(false),
},
statusable,
// Force refresh within 5min window.
dereferencing.Fresh,
)
switch {
case err == nil:
// All fine.
case gtserror.IsNotPermitted(err):
// Reply is straight up not permitted by
// the interaction policy of the status
// it's replying to. Nothing more to do.
log.Debugf(ctx,
"dropping unpermitted ReplyRequest with instrument %s",
replyURI,
)
return nil
default:
// There's some real error.
return gtserror.Newf(
"error processing ReplyRequest with instrument %s: %w",
replyURI, err,
)
}
// The reply is permitted. Check if we
// should send out an Accept immediately.
manualApproval := *reply.PendingApproval && !reply.PreApproved
if manualApproval {
// The reply requires manual approval.
//
// Just notify target account about
// the requested interaction.
if err := p.surface.notifyPendingReply(ctx, reply); err != nil {
return gtserror.Newf("error notifying pending reply: %w", err)
}
return nil
}
// The reply is automatically approved,
// handle side effects of this.
req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
}
// Mark the request as accepted.
req.AcceptedAt = time.Now()
req.ResponseURI = uris.GenerateURIForAccept(
req.TargetAccount.Username, req.ID,
)
req.AuthorizationURI = uris.GenerateURIForAuthorization(
req.TargetAccount.Username, req.ID,
)
// Update in the db.
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"accepted_at",
"response_uri",
"authorization_uri",
); err != nil {
return gtserror.Newf("db error updating interaction request: %w", err)
}
// Send out the accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating accept: %v", err)
}
// Update stats for the replying account.
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, reply); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline the reply + notify recipient(s).
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
return nil
}
func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Cast poll vote type from the worker message.
vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote)
@ -430,18 +565,18 @@ func (p *fediAPI) UpdatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
}
// Get the origin status.
status := vote.Poll.Status
reply := vote.Poll.Status
if *status.Local {
if *reply.Local {
// These were poll votes in a local status, we need to
// federate the updated status model with latest vote counts.
if err := p.federate.UpdateStatus(ctx, status); err != nil {
if err := p.federate.UpdateStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error federating status update: %v", err)
}
}
// Interaction counts changed, uncache from timelines.
p.surface.invalidateStatusFromTimelines(status.ID)
p.surface.invalidateStatusFromTimelines(reply.ID)
return nil
}
@ -503,6 +638,8 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAP
return nil
}
// CreateLike handles an impolite Like, ie., a Like sent directly.
// This is different from the CreateLikeRequest function, which handles polite LikeRequests.
func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
fave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
@ -525,7 +662,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
if err := p.utils.requestFave(ctx, fave); err != nil {
if err := p.utils.impoliteFaveRequest(ctx, fave); err != nil {
return gtserror.Newf("error pending fave: %w", err)
}
@ -540,20 +677,24 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// collection. Do the Accept immediately and
// then process everything else as normal.
// Store an already-accepted interaction request.
id := id.NewULID()
// Store an already-accepted
// impolite interaction request.
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: fave.StatusID,
TargetAccountID: fave.TargetAccountID,
TargetAccount: fave.TargetAccount,
InteractingAccountID: fave.AccountID,
InteractingAccount: fave.Account,
InteractionURI: fave.URI,
InteractionType: gtsmodel.InteractionLike,
Like: fave,
URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: fave.StatusID,
TargetAccountID: fave.TargetAccountID,
TargetAccount: fave.TargetAccount,
InteractingAccountID: fave.AccountID,
InteractingAccount: fave.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
InteractionURI: fave.URI,
InteractionType: gtsmodel.InteractionLike,
Polite: util.Ptr(false),
Like: fave,
ResponseURI: uris.GenerateURIForAccept(fave.TargetAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(fave.TargetAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -562,7 +703,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// Mark the fave itself as now approved.
fave.PendingApproval = util.Ptr(false)
fave.PreApproved = false
fave.ApprovedByURI = approval.URI
fave.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatusFave(
ctx,
fave,
@ -591,6 +732,87 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
return nil
}
// CreateLikeRequest handles a polite LikeRequest, as
// opposed to CreateLike, which handles *impolite* like
// requests (ie., Likes sent directly).
func (p *fediAPI) CreateLikeRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
}
// At this point the not-yet-approved
// interaction request, and the pending
// fave, are both in the database.
if !req.Like.PreApproved {
// The fave is *not* pre-approved, and
// therefore requires manual approval.
//
// Just notify target account about
// the requested interaction.
if err := p.surface.notifyPendingFave(ctx, req.Like); err != nil {
return gtserror.Newf("error notifying pending like: %w", err)
}
return nil
}
// If it's pre-approved on the other hand
// we can handle everything immediately.
// Mark the request as accepted.
req.AcceptedAt = time.Now()
req.ResponseURI = uris.GenerateURIForAccept(
req.TargetAccount.Username, req.ID,
)
req.AuthorizationURI = uris.GenerateURIForAuthorization(
req.TargetAccount.Username, req.ID,
)
// Update in the db.
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"accepted_at",
"response_uri",
"authorization_uri",
); err != nil {
return gtserror.Newf("db error updating interaction request: %w", err)
}
// Send out the accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating accept: %v", err)
}
// Mark the fave as approved.
req.Like.PendingApproval = util.Ptr(false)
req.Like.ApprovedByURI = req.AuthorizationURI
req.Like.PreApproved = false
// Update in the db.
if err := p.state.DB.UpdateStatusFave(
ctx,
req.Like,
"pending_approval",
"approved_by_uri",
); err != nil {
return gtserror.Newf("db error updating status fave: %w", err)
}
// Notify the faved account.
if err := p.surface.notifyFave(ctx, req.Like); err != nil {
log.Errorf(ctx, "error notifying fave: %v", err)
}
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(req.Like.StatusID)
return nil
}
func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
@ -610,7 +832,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
)
if err != nil {
if gtserror.IsUnretrievable(err) ||
gtserror.NotPermitted(err) {
gtserror.IsNotPermitted(err) {
// Boosted status domain blocked, or
// otherwise not permitted, nothing to do.
log.Debugf(ctx, "skipping announce: %v", err)
@ -632,7 +854,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
if err := p.utils.requestAnnounce(ctx, boost); err != nil {
if err := p.utils.impoliteAnnounceRequest(ctx, boost); err != nil {
return gtserror.Newf("error pending boost: %w", err)
}
@ -647,20 +869,24 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// collection. Do the Accept immediately and
// then process everything else as normal.
// Store an already-accepted interaction request.
id := id.NewULID()
// Store an already-accepted
// impolite interaction request.
requestID := id.NewULID()
approval := &gtsmodel.InteractionRequest{
ID: id,
StatusID: boost.BoostOfID,
TargetAccountID: boost.BoostOfAccountID,
TargetAccount: boost.BoostOfAccount,
InteractingAccountID: boost.AccountID,
InteractingAccount: boost.Account,
InteractionURI: boost.URI,
InteractionType: gtsmodel.InteractionAnnounce,
Announce: boost,
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
AcceptedAt: time.Now(),
ID: requestID,
TargetStatusID: boost.BoostOfID,
TargetAccountID: boost.BoostOfAccountID,
TargetAccount: boost.BoostOfAccount,
InteractingAccountID: boost.AccountID,
InteractingAccount: boost.Account,
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(boost.URI, gtsmodel.AnnounceRequestSuffix),
InteractionURI: boost.URI,
InteractionType: gtsmodel.InteractionAnnounce,
Polite: util.Ptr(false),
Announce: boost,
ResponseURI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, requestID),
AuthorizationURI: uris.GenerateURIForAuthorization(boost.BoostOfAccount.Username, requestID),
AcceptedAt: time.Now(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
@ -669,7 +895,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// Mark the boost itself as now approved.
boost.PendingApproval = util.Ptr(false)
boost.PreApproved = false
boost.ApprovedByURI = approval.URI
boost.ApprovedByURI = approval.AuthorizationURI
if err := p.state.DB.UpdateStatus(
ctx,
boost,
@ -708,6 +934,103 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
return nil
}
func (p *fediAPI) CreateAnnounceRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
req, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
}
// At this point the not-yet-handled interaction req
// is in the database, but the announce isn't yet.
//
// We can check permissions for the announce *and*
// put it in the db (if acceptable) by doing Enrich.
boost, err := p.federate.EnrichAnnounce(
ctx,
req.Announce,
fMsg.Receiving.Username,
)
switch {
case err == nil:
// All fine.
case gtserror.IsNotPermitted(err):
// Announce is straight up not permitted
// by the interaction policy of the status
// it's targeting. Nothing more to do.
log.Debugf(ctx,
"dropping unpermitted AnnounceRequest with instrument %s",
req.Announce.URI,
)
return nil
default:
// There's some real error.
return gtserror.Newf(
"error processing AnnounceRequest with instrument %s: %w",
req.Announce.URI, err,
)
}
// The announce is permitted. Check if we
// should send out an Accept immediately.
manualApproval := *boost.PendingApproval && !boost.PreApproved
if manualApproval {
// The announce requires manual approval.
//
// Just notify target account about
// the requested interaction.
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
return gtserror.Newf("error notifying pending announce: %w", err)
}
return nil
}
// The announce is automatically approved,
// mark the request as accepted.
req.AcceptedAt = time.Now()
req.ResponseURI = uris.GenerateURIForAccept(
req.TargetAccount.Username, req.ID,
)
req.AuthorizationURI = uris.GenerateURIForAuthorization(
req.TargetAccount.Username, req.ID,
)
// Update in the db.
if err := p.state.DB.UpdateInteractionRequest(
ctx,
req,
"accepted_at",
"response_uri",
"authorization_uri",
); err != nil {
return gtserror.Newf("db error updating interaction request: %w", err)
}
// Send out the accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating accept: %v", err)
}
// Update stats for the boosting account.
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline the boost + notify recipient(s).
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(boost.BoostOfID)
return nil
}
func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
block, ok := fMsg.GTSModel.(*gtsmodel.Block)
if !ok {
@ -842,29 +1165,29 @@ func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) er
}
func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error {
status, ok := fMsg.GTSModel.(*gtsmodel.Status)
reply, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
// Update stats for the actor account.
if err := p.utils.incrementStatusesCount(ctx, status.Account, status); err != nil {
if err := p.utils.incrementStatusesCount(ctx, reply.Account, reply); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline and notify the status.
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Send out the reply again, fully this time.
if err := p.federate.CreateStatus(ctx, status); err != nil {
if err := p.federate.CreateStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
// Interaction counts changed on the replied-to status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(status.InReplyToID)
p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
return nil
}
@ -893,9 +1216,9 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
// barebones status and insert it into the database,
// if indeed it's actually a status URI we can fetch.
//
// This will also check whether the given AcceptIRI
// This will also check whether the given approvedByURI
// actually grants permission for this status.
status, _, err := p.federate.RefreshStatus(ctx,
reply, _, err := p.federate.RefreshStatus(ctx,
fMsg.Receiving.Username,
bareStatus,
nil, nil,
@ -906,23 +1229,73 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
// No error means it was indeed a remote status, and the
// given approvedByURI permitted it. Timeline and notify it.
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Interaction counts changed on the interacted status;
// uncache the prepared version from all timelines.
if status.InReplyToID != "" {
p.surface.invalidateStatusFromTimelines(status.InReplyToID)
if reply.InReplyToID != "" {
p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
}
if status.BoostOfID != "" {
p.surface.invalidateStatusFromTimelines(status.BoostOfID)
if reply.BoostOfID != "" {
p.surface.invalidateStatusFromTimelines(reply.BoostOfID)
}
return nil
}
func (p *fediAPI) AcceptPoliteReplyRequest(ctx context.Context, fMsg *messages.FromFediAPI) error {
if util.IsNil(fMsg.GTSModel) {
// If the interaction request is nil, this
// must be an accept of a remote ReplyRequest
// not targeting one of our statuses.
//
// Just pass it to the AcceptRemoteStatus
// func to do dereferencing + side effects.
log.Debug(ctx, "accepting remote ReplyRequest for remote reply")
return p.AcceptRemoteStatus(ctx, fMsg)
}
// If the interaction request is not nil, this will
// be an accept of one of our replies to a remote.
//
// Since the int req + reply have already been updated
// in the federatingDB, we just need to do side effects.
intReq, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel)
}
// Ensure reply populated.
reply := intReq.Reply
if err := p.state.DB.PopulateStatus(ctx, reply); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
// Update stats for the actor account.
if err := p.utils.incrementStatusesCount(ctx, reply.Account, reply); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline and notify the status.
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Send out the reply with approval attached.
if err := p.federate.CreateStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
// Interaction counts changed on the replied-to status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(reply.InReplyToID)
return nil
}
func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
@ -1169,7 +1542,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e
// be in the database, we just need to do side effects.
// Get the rejected status.
status, err := p.state.DB.GetStatusByURI(
reply, err := p.state.DB.GetStatusByURI(
gtscontext.SetBarebones(ctx),
req.InteractionURI,
)
@ -1189,7 +1562,7 @@ func (p *fediAPI) RejectReply(ctx context.Context, fMsg *messages.FromFediAPI) e
// Perform the actual status deletion.
if err := p.utils.wipeStatus(
ctx,
status,
reply,
deleteAttachments,
copyToSinBin,
); err != nil {

View file

@ -18,6 +18,7 @@
package workers_test
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -26,11 +27,13 @@ import (
"testing"
"time"
"code.superseriousbusiness.org/activity/streams/vocab"
"code.superseriousbusiness.org/gotosocial/internal/ap"
apimodel "code.superseriousbusiness.org/gotosocial/internal/api/model"
"code.superseriousbusiness.org/gotosocial/internal/db"
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
"code.superseriousbusiness.org/gotosocial/internal/id"
"code.superseriousbusiness.org/gotosocial/internal/messages"
"code.superseriousbusiness.org/gotosocial/internal/stream"
"code.superseriousbusiness.org/gotosocial/internal/util"
@ -781,6 +784,123 @@ func (suite *FromFediAPITestSuite) TestUpdateNote() {
}
}
func (suite *FromFediAPITestSuite) TestCreateReplyRequest() {
var (
ctx = suite.T().Context()
testStructs = testrig.SetupTestStructs(rMediaPath, rTemplatePath)
requesting = suite.testAccounts["remote_account_1"]
receiving = suite.testAccounts["admin_account"]
testStatus = suite.testStatuses["admin_account_status_1"]
intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219"
intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219"
jsonStr = `{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://gotosocial.org/ns",
{
"sensitive": "as:sensitive"
}
],
"type": "ReplyRequest",
"id": "` + intReqURI + `",
"actor": "` + requesting.URI + `",
"object": "` + testStatus.URI + `",
"to": "` + receiving.URI + `",
"instrument": {
"attributedTo": "` + requesting.URI + `",
"cc": "` + requesting.FollowersURI + `",
"content": "\u003cp\u003ethis is a reply!\u003c/p\u003e",
"id": "` + intURI + `",
"inReplyTo": "` + testStatus.URI + `",
"tag": {
"href": "` + receiving.URI + `",
"name": "@` + receiving.Username + `@localhost:8080",
"type": "Mention"
},
"to": "https://www.w3.org/ns/activitystreams#Public",
"type": "Note"
}
}`
)
defer testrig.TearDownTestStructs(testStructs)
suite.T().Logf("testing reply request:\n\n%s", jsonStr)
// Decode the reply request + embedded statusable.
t, err := ap.DecodeType(ctx, io.NopCloser(bytes.NewBufferString(jsonStr)))
if err != nil {
suite.FailNow(err.Error())
}
replyReq := t.(vocab.GoToSocialReplyRequest)
statusable := replyReq.GetActivityStreamsInstrument().At(0).GetActivityStreamsNote().(ap.Statusable)
// Create a pending interaction request in the
// database, as though the reply req had already
// passed through the federatingdb function.
intReq := &gtsmodel.InteractionRequest{
ID: id.NewULID(),
TargetStatusID: testStatus.ID,
TargetStatus: testStatus,
TargetAccountID: receiving.ID,
TargetAccount: receiving,
InteractingAccountID: requesting.ID,
InteractingAccount: requesting,
InteractionRequestURI: intReqURI,
InteractionURI: ap.GetJSONLDId(statusable).String(),
InteractionType: gtsmodel.InteractionReply,
Polite: util.Ptr(true),
Reply: nil, // Not settable yet.
}
if err := testStructs.State.DB.PutInteractionRequest(ctx, intReq); err != nil {
suite.FailNow(err.Error())
}
// Process the message.
if err = testStructs.Processor.Workers().ProcessFromFediAPI(
ctx,
&messages.FromFediAPI{
APObjectType: ap.ActivityReplyRequest,
APActivityType: ap.ActivityCreate,
GTSModel: intReq,
APObject: statusable,
Receiving: receiving,
Requesting: requesting,
},
); err != nil {
suite.FailNow(err.Error())
}
// The interaction request should be accepted.
intReq, err = testStructs.State.DB.GetInteractionRequestByID(ctx, intReq.ID)
if err != nil {
suite.FailNow(err.Error())
}
suite.WithinDuration(time.Now(), intReq.AcceptedAt, 1*time.Minute)
suite.NotEmpty(intReq.AuthorizationURI)
suite.NotEmpty(intReq.ResponseURI)
// Federator should send out an Accept that looks something like:
//
// {
// "@context": [
// "https://gotosocial.org/ns",
// "https://www.w3.org/ns/activitystreams"
// ],
// "actor": "http://localhost:8080/users/admin",
// "id": "http://localhost:8080/users/admin/accepts/01K2CV90660VRPZM39R35NMSG9",
// "object": {
// "actor": "http://fossbros-anonymous.io/users/foss_satan",
// "id": "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219",
// "instrument": "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219",
// "object": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
// "type": "ReplyRequest"
// },
// "result": "http://localhost:8080/users/admin/authorizations/01K2CV90660VRPZM39R35NMSG9",
// "to": "http://fossbros-anonymous.io/users/foss_satan",
// "type": "Accept"
// }
}
func TestFromFederatorTestSuite(t *testing.T) {
suite.Run(t, &FromFediAPITestSuite{})
}

View file

@ -526,9 +526,13 @@ func (u *utils) decrementFollowRequestsCount(
return nil
}
// requestFave stores an interaction request
// impoliteFaveRequest stores an interaction request
// for the given fave, and notifies the interactee.
func (u *utils) requestFave(
//
// It should be used only when an actor has sent a Like
// directly in response to a post that requires approval
// for it, instead of sending a LikeRequest.
func (u *utils) impoliteFaveRequest(
ctx context.Context,
fave *gtsmodel.StatusFave,
) error {
@ -555,8 +559,8 @@ func (u *utils) requestFave(
return nil
}
// Create + store new interaction request.
req = typeutils.StatusFaveToInteractionRequest(fave)
// Create + store new impolite interaction request.
req = typeutils.StatusFaveToImpoliteInteractionRequest(fave)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}
@ -569,9 +573,13 @@ func (u *utils) requestFave(
return nil
}
// requestReply stores an interaction request
// impoliteReplyRequest stores an interaction request
// for the given reply, and notifies the interactee.
func (u *utils) requestReply(
//
// It should be used only when an actor has sent a reply
// directly in response to a post that requires approval
// for it, instead of sending a ReplyRequest.
func (u *utils) impoliteReplyRequest(
ctx context.Context,
reply *gtsmodel.Status,
) error {
@ -598,8 +606,8 @@ func (u *utils) requestReply(
return nil
}
// Create + store interaction request.
req = typeutils.StatusToInteractionRequest(reply)
// Create + store impolite interaction request.
req = typeutils.StatusToImpoliteInteractionRequest(reply)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}
@ -612,9 +620,13 @@ func (u *utils) requestReply(
return nil
}
// requestAnnounce stores an interaction request
// impoliteAnnounceRequest stores an interaction request
// for the given announce, and notifies the interactee.
func (u *utils) requestAnnounce(
//
// It should be used only when an actor has sent an Announce
// directly in response to a post that requires approval
// for it, instead of sending an AnnounceRequest.
func (u *utils) impoliteAnnounceRequest(
ctx context.Context,
boost *gtsmodel.Status,
) error {
@ -641,8 +653,8 @@ func (u *utils) requestAnnounce(
return nil
}
// Create + store interaction request.
req = typeutils.StatusToInteractionRequest(boost)
// Create + store impolite interaction request.
req = typeutils.StatusToImpoliteInteractionRequest(boost)
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
return gtserror.Newf("db error storing interaction request: %w", err)
}