[feature] Interaction requests client api + settings panel (#3215)

* [feature] Interaction requests client api + settings panel

* test accept / reject

* fmt

* don't pin rejected interaction

* use single db model for interaction accept, reject, and request

* swaggor

* env sharting

* append errors

* remove ErrNoEntries checks

* change intReqID to reqID

* rename "pend" to "request"

* markIntsPending -> mark interactionsPending

* use log instead of returning error when rejecting interaction

* empty migration

* jolly renaming

* make interactionURI unique again

* swag grr

* remove unnecessary locks

* invalidate as last step
This commit is contained in:
tobi 2024-08-24 11:49:37 +02:00 committed by GitHub
commit f23f04e0b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 4446 additions and 663 deletions

View file

@ -20,6 +20,7 @@ package typeutils
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@ -97,3 +98,80 @@ func (c *Converter) StatusToBoost(
return boost, nil
}
func StatusToInteractionRequest(
ctx context.Context,
status *gtsmodel.Status,
) (*gtsmodel.InteractionRequest, error) {
reqID, err := id.NewULIDFromTime(status.CreatedAt)
if err != nil {
return nil, gtserror.Newf("error generating ID: %w", err)
}
var (
targetID string
target *gtsmodel.Status
targetAccountID string
targetAccount *gtsmodel.Account
interactionType gtsmodel.InteractionType
reply *gtsmodel.Status
announce *gtsmodel.Status
)
if status.InReplyToID != "" {
// It's a reply.
targetID = status.InReplyToID
target = status.InReplyTo
targetAccountID = status.InReplyToAccountID
targetAccount = status.InReplyToAccount
interactionType = gtsmodel.InteractionReply
reply = status
} else {
// It's a boost.
targetID = status.BoostOfID
target = status.BoostOf
targetAccountID = status.BoostOfAccountID
targetAccount = status.BoostOfAccount
interactionType = gtsmodel.InteractionAnnounce
announce = status
}
return &gtsmodel.InteractionRequest{
ID: reqID,
CreatedAt: status.CreatedAt,
StatusID: targetID,
Status: target,
TargetAccountID: targetAccountID,
TargetAccount: targetAccount,
InteractingAccountID: status.AccountID,
InteractingAccount: status.Account,
InteractionURI: status.URI,
InteractionType: interactionType,
Reply: reply,
Announce: announce,
}, nil
}
func StatusFaveToInteractionRequest(
ctx context.Context,
fave *gtsmodel.StatusFave,
) (*gtsmodel.InteractionRequest, error) {
reqID, err := id.NewULIDFromTime(fave.CreatedAt)
if err != nil {
return nil, gtserror.Newf("error generating ID: %w", err)
}
return &gtsmodel.InteractionRequest{
ID: reqID,
CreatedAt: fave.CreatedAt,
StatusID: fave.StatusID,
Status: fave.Status,
TargetAccountID: fave.TargetAccountID,
TargetAccount: fave.TargetAccount,
InteractingAccountID: fave.AccountID,
InteractingAccount: fave.Account,
InteractionURI: fave.URI,
InteractionType: gtsmodel.InteractionLike,
Like: fave,
}, nil
}

View file

@ -1960,36 +1960,36 @@ func (c *Converter) InteractionPolicyToASInteractionPolicy(
return policy, nil
}
// InteractionApprovalToASAccept converts a *gtsmodel.InteractionApproval
// InteractionReqToASAccept converts a *gtsmodel.InteractionRequest
// to an ActivityStreams Accept, addressed to the interacting account.
func (c *Converter) InteractionApprovalToASAccept(
func (c *Converter) InteractionReqToASAccept(
ctx context.Context,
approval *gtsmodel.InteractionApproval,
req *gtsmodel.InteractionRequest,
) (vocab.ActivityStreamsAccept, error) {
accept := streams.NewActivityStreamsAccept()
acceptID, err := url.Parse(approval.URI)
acceptID, err := url.Parse(req.URI)
if err != nil {
return nil, gtserror.Newf("invalid accept uri: %w", err)
}
actorIRI, err := url.Parse(approval.Account.URI)
actorIRI, err := url.Parse(req.TargetAccount.URI)
if err != nil {
return nil, gtserror.Newf("invalid account uri: %w", err)
}
objectIRI, err := url.Parse(approval.InteractionURI)
objectIRI, err := url.Parse(req.InteractionURI)
if err != nil {
return nil, gtserror.Newf("invalid target uri: %w", err)
}
toIRI, err := url.Parse(approval.InteractingAccount.URI)
toIRI, err := url.Parse(req.InteractingAccount.URI)
if err != nil {
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
}
// Set id to the URI of
// interactionApproval.
// interaction request.
ap.SetJSONLDId(accept, acceptID)
// Actor is the account that

View file

@ -1057,26 +1057,26 @@ func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
}`, string(bytes))
}
func (suite *InternalToASTestSuite) TestInteractionApprovalToASAccept() {
func (suite *InternalToASTestSuite) TestInteractionReqToASAccept() {
acceptingAccount := suite.testAccounts["local_account_1"]
interactingAccount := suite.testAccounts["remote_account_1"]
interactionApproval := &gtsmodel.InteractionApproval{
req := &gtsmodel.InteractionRequest{
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
UpdatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
AccountID: acceptingAccount.ID,
Account: acceptingAccount,
TargetAccountID: acceptingAccount.ID,
TargetAccount: acceptingAccount,
InteractingAccountID: interactingAccount.ID,
InteractingAccount: interactingAccount,
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
InteractionType: gtsmodel.InteractionAnnounce,
URI: "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
AcceptedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
}
accept, err := suite.typeconverter.InteractionApprovalToASAccept(
accept, err := suite.typeconverter.InteractionReqToASAccept(
context.Background(),
interactionApproval,
req,
)
if err != nil {
suite.FailNow(err.Error())

View file

@ -2592,3 +2592,74 @@ func policyValsToAPIPolicyVals(vals gtsmodel.PolicyValues) []apimodel.PolicyValu
return apiVals
}
// InteractionReqToAPIInteractionReq converts the given *gtsmodel.InteractionRequest
// to an *apimodel.InteractionRequest, from the perspective of requestingAcct.
func (c *Converter) InteractionReqToAPIInteractionReq(
ctx context.Context,
req *gtsmodel.InteractionRequest,
requestingAcct *gtsmodel.Account,
) (*apimodel.InteractionRequest, error) {
// Ensure interaction request is populated.
if err := c.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
err := gtserror.Newf("error populating: %w", err)
return nil, err
}
interactingAcct, err := c.AccountToAPIAccountPublic(ctx, req.InteractingAccount)
if err != nil {
err := gtserror.Newf("error converting interacting acct: %w", err)
return nil, err
}
interactedStatus, err := c.StatusToAPIStatus(
ctx,
req.Status,
requestingAcct,
statusfilter.FilterContextNone,
nil,
nil,
)
if err != nil {
err := gtserror.Newf("error converting interacted status: %w", err)
return nil, err
}
var reply *apimodel.Status
if req.InteractionType == gtsmodel.InteractionReply {
reply, err = c.StatusToAPIStatus(
ctx,
req.Reply,
requestingAcct,
statusfilter.FilterContextNone,
nil,
nil,
)
if err != nil {
err := gtserror.Newf("error converting reply: %w", err)
return nil, err
}
}
var acceptedAt string
if req.IsAccepted() {
acceptedAt = util.FormatISO8601(req.AcceptedAt)
}
var rejectedAt string
if req.IsRejected() {
rejectedAt = util.FormatISO8601(req.RejectedAt)
}
return &apimodel.InteractionRequest{
ID: req.ID,
Type: req.InteractionType.String(),
CreatedAt: util.FormatISO8601(req.CreatedAt),
Account: interactingAcct,
Status: interactedStatus,
Reply: reply,
AcceptedAt: acceptedAt,
RejectedAt: rejectedAt,
URI: req.URI,
}, nil
}