[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
parent 8e5a72ac5c
commit f23f04e0b1
72 changed files with 4446 additions and 663 deletions

View File

@ -20,18 +20,23 @@ package workers
import (
"context"
"errors"
"time"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@ -44,6 +49,7 @@ type clientAPI struct {
surface *Surface
federate *federate
account *account.Processor
common *common.Processor
utils *utils
}
@ -160,6 +166,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
// REJECT USER (ie., new user+account sign-up)
case ap.ObjectProfile:
return p.clientAPI.RejectUser(ctx, cMsg)
// REJECT NOTE/STATUS (ie., reject a reply)
case ap.ObjectNote:
return p.clientAPI.RejectReply(ctx, cMsg)
// REJECT LIKE
case ap.ActivityLike:
return p.clientAPI.RejectLike(ctx, cMsg)
// REJECT BOOST
case ap.ActivityAnnounce:
return p.clientAPI.RejectAnnounce(ctx, cMsg)
}
// UNDO SOMETHING
@ -261,15 +279,13 @@ 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.
// Notify *local* account of pending reply.
if err := p.surface.notifyPendingReply(ctx, status); err != nil {
log.Errorf(ctx, "error notifying pending reply: %v", err)
if err := p.utils.requestReply(ctx, status); err != nil {
return gtserror.Newf("error pending reply: %w", err)
}
// Send Create to *remote* account inbox ONLY.
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating pending reply: %v", err)
return gtserror.Newf("error federating pending reply: %w", err)
}
// Return early.
@ -285,14 +301,38 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// sending out the Create with the approval
// URI attached.
// Put approval in the database and
// update the status with approvedBy URI.
approval, err := p.utils.approveReply(ctx, status)
if err != nil {
return gtserror.Newf("error pre-approving reply: %w", err)
// Store an already-accepted interaction request.
id := 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.InteractionLike,
Reply: status,
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
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)
}
// Mark the status as now approved.
status.PendingApproval = util.Ptr(false)
status.PreApproved = false
status.ApprovedByURI = approval.URI
if err := p.state.DB.UpdateStatus(
ctx,
status,
"pending_approval",
"approved_by_uri",
); err != nil {
return gtserror.Newf("db error updating status: %w", err)
}
// Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of reply: %w", err)
}
@ -309,16 +349,16 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status: %v", err)
}
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status: %v", err)
}
return nil
}
@ -344,9 +384,6 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
status := vote.Poll.Status
status.Poll = vote.Poll
// Interaction counts changed on the source status, uncache from timelines.
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
if *status.Local {
// These are poll votes in a local status, we only need to
// federate the updated status model with latest vote counts.
@ -360,6 +397,9 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
}
}
// Interaction counts changed on the source status, uncache from timelines.
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
return nil
}
@ -429,10 +469,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// If pending approval is true then fave must
// target a status (either one of ours or a
// remote) that requires approval for the fave.
pendingApproval := util.PtrOrValue(
fave.PendingApproval,
false,
)
pendingApproval := util.PtrOrZero(fave.PendingApproval)
switch {
case pendingApproval && !fave.PreApproved:
@ -442,15 +479,13 @@ 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.
// Notify *local* account of pending reply.
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
log.Errorf(ctx, "error notifying pending fave: %v", err)
if err := p.utils.requestFave(ctx, fave); err != nil {
return gtserror.Newf("error pending fave: %w", err)
}
// Send Like to *remote* account inbox ONLY.
if err := p.federate.Like(ctx, fave); err != nil {
log.Errorf(ctx, "error federating pending Like: %v", err)
return gtserror.Newf("error federating pending Like: %v", err)
}
// Return early.
@ -466,14 +501,38 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// sending out the Like with the approval
// URI attached.
// Put approval in the database and
// update the fave with approvedBy URI.
approval, err := p.utils.approveFave(ctx, fave)
if err != nil {
return gtserror.Newf("error pre-approving fave: %w", err)
// Store an already-accepted interaction request.
id := 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(),
}
if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
}
// Mark the fave itself as now approved.
fave.PendingApproval = util.Ptr(false)
fave.PreApproved = false
fave.ApprovedByURI = approval.URI
if err := p.state.DB.UpdateStatusFave(
ctx,
fave,
"pending_approval",
"approved_by_uri",
); err != nil {
return gtserror.Newf("db error updating status fave: %w", err)
}
// Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of fave: %w", err)
}
@ -485,14 +544,14 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
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(ctx, fave.StatusID)
if err := p.federate.Like(ctx, fave); err != nil {
log.Errorf(ctx, "error federating like: %v", err)
}
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
return nil
}
@ -505,10 +564,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// If pending approval is true then status must
// boost a status (either one of ours or a
// remote) that requires approval for the boost.
pendingApproval := util.PtrOrValue(
boost.PendingApproval,
false,
)
pendingApproval := util.PtrOrZero(boost.PendingApproval)
switch {
case pendingApproval && !boost.PreApproved:
@ -518,15 +574,13 @@ 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.
// Notify *local* account of pending announce.
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
log.Errorf(ctx, "error notifying pending boost: %v", err)
if err := p.utils.requestAnnounce(ctx, boost); err != nil {
return gtserror.Newf("error pending boost: %w", err)
}
// Send Announce to *remote* account inbox ONLY.
if err := p.federate.Announce(ctx, boost); err != nil {
log.Errorf(ctx, "error federating pending Announce: %v", err)
return gtserror.Newf("error federating pending Announce: %v", err)
}
// Return early.
@ -542,14 +596,38 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// sending out the Create with the approval
// URI attached.
// Put approval in the database and
// update the boost with approvedBy URI.
approval, err := p.utils.approveAnnounce(ctx, boost)
if err != nil {
return gtserror.Newf("error pre-approving boost: %w", err)
// Store an already-accepted interaction request.
id := 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.InteractionLike,
Announce: boost,
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
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)
}
// Mark the boost itself as now approved.
boost.PendingApproval = util.Ptr(false)
boost.PreApproved = false
boost.ApprovedByURI = approval.URI
if err := p.state.DB.UpdateStatus(
ctx,
boost,
"pending_approval",
"approved_by_uri",
); err != nil {
return gtserror.Newf("db error updating status: %w", err)
}
// Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of boost: %w", err)
}
@ -572,14 +650,14 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
log.Errorf(ctx, "error notifying boost: %v", err)
}
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
if err := p.federate.Announce(ctx, boost); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
return nil
}
@ -629,9 +707,6 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error federating status update: %v", err)
}
// Status representation has changed, invalidate from timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
if status.Poll != nil && status.Poll.Closing {
// If the latest status has a newly closed poll, at least compared
@ -646,6 +721,9 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error streaming status edit: %v", err)
}
// Status representation has changed, invalidate from timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
return nil
}
@ -791,14 +869,14 @@ func (p *clientAPI) UndoFave(ctx context.Context, cMsg *messages.FromClientAPI)
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
}
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
if err := p.federate.UndoLike(ctx, statusFave); err != nil {
log.Errorf(ctx, "error federating like undo: %v", err)
}
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
return nil
}
@ -821,14 +899,14 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error removing timelined status: %v", err)
}
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
if err := p.federate.UndoAnnounce(ctx, status); err != nil {
log.Errorf(ctx, "error federating announce undo: %v", err)
}
// Interaction counts changed on the boosted status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
return nil
}
@ -874,16 +952,16 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error updating account stats: %v", err)
}
if err := p.federate.DeleteStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status delete: %v", err)
}
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}
if err := p.federate.DeleteStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status delete: %v", err)
}
return nil
}
@ -1050,16 +1128,188 @@ func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI
}
func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
// TODO
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
// Notify the fave (distinct from the notif for the pending fave).
if err := p.surface.notifyFave(ctx, req.Like); err != nil {
log.Errorf(ctx, "error notifying fave: %v", err)
}
// Send out the Accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating approval of like: %v", err)
}
// Interaction counts changed on the faved status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, req.Like.StatusID)
return nil
}
func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
// TODO
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
var (
interactingAcct = req.InteractingAccount
reply = req.Reply
)
// Update stats for the reply author account.
if err := p.utils.incrementStatusesCount(ctx, interactingAcct, reply); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline the reply + notify relevant accounts.
if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
log.Errorf(ctx, "error timelining and notifying status reply: %v", err)
}
// Send out the Accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating approval of reply: %v", err)
}
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, reply.InReplyToID)
return nil
}
func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
// TODO
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
var (
interactingAcct = req.InteractingAccount
boost = req.Announce
)
// Update stats for the boost author account.
if err := p.utils.incrementStatusesCount(ctx, interactingAcct, boost); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Timeline and notify the announce.
if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// Notify the announce (distinct from the notif for the pending announce).
if err := p.surface.notifyAnnounce(ctx, boost); err != nil {
log.Errorf(ctx, "error notifying announce: %v", err)
}
// Send out the Accept.
if err := p.federate.AcceptInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating approval of announce: %v", err)
}
// Interaction counts changed on the original status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
return nil
}
func (p *clientAPI) RejectLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
// At this point the InteractionRequest should already
// be in the database, we just need to do side effects.
// Send out the Reject.
if err := p.federate.RejectInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating rejection of like: %v", err)
}
// Get the rejected fave.
fave, err := p.state.DB.GetStatusFaveByURI(
gtscontext.SetBarebones(ctx),
req.InteractionURI,
)
if err != nil {
return gtserror.Newf("db error getting rejected fave: %w", err)
}
// Delete the status fave.
if err := p.state.DB.DeleteStatusFaveByID(ctx, fave.ID); err != nil {
return gtserror.Newf("db error deleting status fave: %w", err)
}
return nil
}
func (p *clientAPI) RejectReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
// At this point the InteractionRequest should already
// be in the database, we just need to do side effects.
// Send out the Reject.
if err := p.federate.RejectInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating rejection of reply: %v", err)
}
// Get the rejected status.
status, err := p.state.DB.GetStatusByURI(
gtscontext.SetBarebones(ctx),
req.InteractionURI,
)
if err != nil {
return gtserror.Newf("db error getting rejected reply: %w", err)
}
// Totally wipe the status.
if err := p.utils.wipeStatus(ctx, status, true); err != nil {
return gtserror.Newf("error wiping status: %w", err)
}
return nil
}
func (p *clientAPI) RejectAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
}
// At this point the InteractionRequest should already
// be in the database, we just need to do side effects.
// Send out the Reject.
if err := p.federate.RejectInteraction(ctx, req); err != nil {
log.Errorf(ctx, "error federating rejection of announce: %v", err)
}
// Get the rejected boost.
boost, err := p.state.DB.GetStatusByURI(
gtscontext.SetBarebones(ctx),
req.InteractionURI,
)
if err != nil {
return gtserror.Newf("db error getting rejected announce: %w", err)
}
// Totally wipe the status.
if err := p.utils.wipeStatus(ctx, boost, true); err != nil {
return gtserror.Newf("error wiping status: %w", err)
}
return nil
}