mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[feature] Federate interaction policies + Accepts; enforce policies (#3138)
* [feature] Federate interaction policies + Accepts; enforce policies * use Acceptable type * fix index * remove appendIRIStrs * add GetAccept federatingdb function * lock on object IRI
This commit is contained in:
84
internal/processing/fedi/accept.go
Normal file
84
internal/processing/fedi/accept.go
Normal file
@ -0,0 +1,84 @@
|
||||
// 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"
|
||||
"errors"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// AcceptGet handles the getting of a fedi/activitypub
|
||||
// representation of a local interaction approval.
|
||||
//
|
||||
// It performs appropriate authentication before
|
||||
// returning a JSON serializable interface.
|
||||
func (p *Processor) AcceptGet(
|
||||
ctx context.Context,
|
||||
requestedUser string,
|
||||
approvalID string,
|
||||
) (interface{}, 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)
|
||||
}
|
||||
|
||||
receivingAcct := auth.receivingAcct
|
||||
|
||||
approval, err := p.state.DB.GetInteractionApprovalByID(ctx, approvalID)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting approval %s: %w", approvalID, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if approval.AccountID != receivingAcct.ID {
|
||||
const text = "approval does not belong to receiving account"
|
||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
||||
}
|
||||
|
||||
if approval == nil {
|
||||
err := gtserror.Newf("approval %s not found", approvalID)
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
accept, err := p.converter.InteractionApprovalToASAccept(ctx, approval)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error converting approval: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
data, err := ap.Serialize(accept)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error serializing accept: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
@ -177,8 +177,7 @@ func NewProcessor(
|
||||
visFilter *visibility.Filter,
|
||||
intFilter *interaction.Filter,
|
||||
) *Processor {
|
||||
var parseMentionFunc = GetParseMentionFunc(state, federator)
|
||||
|
||||
parseMentionFunc := GetParseMentionFunc(state, federator)
|
||||
processor := &Processor{
|
||||
converter: converter,
|
||||
oauthServer: oauthServer,
|
||||
|
@ -104,9 +104,18 @@ func (p *Processor) BoostCreate(
|
||||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *target.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
boost.PreApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
|
@ -221,9 +221,18 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
|
||||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *inReplyTo.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
status.PreApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
|
@ -103,8 +103,13 @@ func (p *Processor) FaveCreate(
|
||||
return nil, gtserror.NewErrorForbidden(err, errText)
|
||||
}
|
||||
|
||||
// Derive pendingApproval status.
|
||||
var pendingApproval bool
|
||||
// Derive pendingApproval
|
||||
// and preapproved status.
|
||||
var (
|
||||
pendingApproval bool
|
||||
preApproved bool
|
||||
)
|
||||
|
||||
switch {
|
||||
case policyResult.WithApproval():
|
||||
// We're allowed to do
|
||||
@ -115,9 +120,18 @@ func (p *Processor) FaveCreate(
|
||||
// We're permitted to do this, but since
|
||||
// we matched due to presence in a followers
|
||||
// or following collection, we should mark
|
||||
// as pending approval and wait for an accept.
|
||||
// as pending approval and wait until we can
|
||||
// prove it's been Accepted by the target.
|
||||
pendingApproval = true
|
||||
|
||||
if *status.Local {
|
||||
// If the target is local we don't need
|
||||
// to wait for an Accept from remote,
|
||||
// we can just preapprove it and have
|
||||
// the processor create the Accept.
|
||||
preApproved = true
|
||||
}
|
||||
|
||||
case policyResult.Permitted():
|
||||
// We're permitted to do this
|
||||
// based on another kind of match.
|
||||
@ -138,6 +152,7 @@ func (p *Processor) FaveCreate(
|
||||
StatusID: status.ID,
|
||||
Status: status,
|
||||
URI: uris.GenerateURIForLike(requester.Username, faveID),
|
||||
PreApproved: preApproved,
|
||||
PendingApproval: &pendingApproval,
|
||||
}
|
||||
|
||||
|
@ -23,12 +23,14 @@ import (
|
||||
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// federate wraps functions for federating
|
||||
@ -135,6 +137,12 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStatus sends the given status out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the status is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it replies to,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||
// Do nothing if the status
|
||||
// shouldn't be federated.
|
||||
@ -153,18 +161,32 @@ func (f *federate) CreateStatus(ctx context.Context, status *gtsmodel.Status) er
|
||||
return gtserror.Newf("error populating status: %w", err)
|
||||
}
|
||||
|
||||
// Parse the outbox URI of the status author.
|
||||
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert status to AS Statusable implementing type.
|
||||
statusable, err := f.converter.StatusToAS(ctx, status)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting status to Statusable: %w", err)
|
||||
}
|
||||
|
||||
// If status is pending approval,
|
||||
// it must be a reply. Deliver it
|
||||
// **ONLY** to the account it replies
|
||||
// to, on behalf of the replier.
|
||||
if util.PtrOrValue(status.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
status.Account,
|
||||
status.InReplyToAccount,
|
||||
// Status has to be wrapped in Create activity.
|
||||
typeutils.WrapStatusableInCreate(statusable, false),
|
||||
)
|
||||
}
|
||||
|
||||
// Parse the outbox URI of the status author.
|
||||
outboxIRI, err := parseURI(status.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send a Create activity with Statusable via the Actor's outbox.
|
||||
create := typeutils.WrapStatusableInCreate(statusable, false)
|
||||
if _, err := f.FederatingActor().Send(ctx, outboxIRI, create); err != nil {
|
||||
@ -672,6 +694,12 @@ func (f *federate) RejectFollow(ctx context.Context, follow *gtsmodel.Follow) er
|
||||
return nil
|
||||
}
|
||||
|
||||
// Like sends the given fave out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the fave is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it faves,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
@ -684,18 +712,30 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(fave.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the ActivityStreams Like.
|
||||
like, err := f.converter.FaveToAS(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error converting fave to AS Like: %w", err)
|
||||
}
|
||||
|
||||
// If fave is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it faves, on behalf of the faver.
|
||||
if util.PtrOrValue(fave.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
fave.Account,
|
||||
fave.TargetAccount,
|
||||
like,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(fave.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the Like via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, like,
|
||||
@ -709,6 +749,12 @@ func (f *federate) Like(ctx context.Context, fave *gtsmodel.StatusFave) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Announce sends the given boost out to relevant
|
||||
// recipients with the Outbox of the status creator.
|
||||
//
|
||||
// If the boost is pending approval, then it will be
|
||||
// sent **ONLY** to the inbox of the account it boosts,
|
||||
// ignoring shared inboxes.
|
||||
func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateStatus(ctx, boost); err != nil {
|
||||
@ -721,12 +767,6 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(boost.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the ActivityStreams Announce.
|
||||
announce, err := f.converter.BoostToAS(
|
||||
ctx,
|
||||
@ -738,6 +778,24 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
return gtserror.Newf("error converting boost to AS: %w", err)
|
||||
}
|
||||
|
||||
// If announce is pending approval,
|
||||
// deliver it **ONLY** to the account
|
||||
// it boosts, on behalf of the booster.
|
||||
if util.PtrOrValue(boost.PendingApproval, false) {
|
||||
return f.deliverToInboxOnly(
|
||||
ctx,
|
||||
boost.Account,
|
||||
boost.BoostOfAccount,
|
||||
announce,
|
||||
)
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(boost.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send the Announce via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, announce,
|
||||
@ -751,6 +809,57 @@ func (f *federate) Announce(ctx context.Context, boost *gtsmodel.Status) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// deliverToInboxOnly delivers the given Activity
|
||||
// *only* to the inbox of targetAcct, on behalf of
|
||||
// sendingAcct, regardless of the `to` and `cc` values
|
||||
// set on the activity. This should be used specifically
|
||||
// for sending "pending approval" activities.
|
||||
func (f *federate) deliverToInboxOnly(
|
||||
ctx context.Context,
|
||||
sendingAcct *gtsmodel.Account,
|
||||
targetAcct *gtsmodel.Account,
|
||||
t vocab.Type,
|
||||
) error {
|
||||
if targetAcct.IsLocal() {
|
||||
// If this is a local target,
|
||||
// they've already received it.
|
||||
return nil
|
||||
}
|
||||
|
||||
toInbox, err := url.Parse(targetAcct.InboxURI)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error parsing target inbox uri: %w",
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
tsport, err := f.TransportController().NewTransportForUsername(
|
||||
ctx,
|
||||
sendingAcct.Username,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf(
|
||||
"error getting transport to deliver activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
m, err := ap.Serialize(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tsport.Deliver(ctx, m, toInbox); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error delivering activity %T to target inbox %s: %w",
|
||||
t, targetAcct.InboxURI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) UpdateAccount(ctx context.Context, account *gtsmodel.Account) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateAccount(ctx, account); err != nil {
|
||||
@ -1015,3 +1124,75 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *federate) AcceptInteraction(
|
||||
ctx context.Context,
|
||||
approval *gtsmodel.InteractionApproval,
|
||||
) error {
|
||||
// Populate model.
|
||||
if err := f.state.DB.PopulateInteractionApproval(ctx, approval); err != nil {
|
||||
return gtserror.Newf("error populating approval: %w", err)
|
||||
}
|
||||
|
||||
// Bail if interacting account is ours:
|
||||
// we've already accepted internally and
|
||||
// shouldn't send an Accept to ourselves.
|
||||
if approval.InteractingAccount.IsLocal() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bail if account isn't ours:
|
||||
// we can't Accept on another
|
||||
// instance's behalf. (This
|
||||
// should never happen but...)
|
||||
if approval.Account.IsRemote() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse relevant URI(s).
|
||||
outboxIRI, err := parseURI(approval.Account.OutboxURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
acceptingAcctIRI, err := parseURI(approval.Account.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interactingAcctURI, err := parseURI(approval.InteractingAccount.URI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interactionURI, err := parseURI(approval.InteractionURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create a new Accept.
|
||||
accept := streams.NewActivityStreamsAccept()
|
||||
|
||||
// Set interacted-with account
|
||||
// as Actor of the Accept.
|
||||
ap.AppendActorIRIs(accept, acceptingAcctIRI)
|
||||
|
||||
// Set the interacted-with object
|
||||
// as Object of the Accept.
|
||||
ap.AppendObjectIRIs(accept, interactionURI)
|
||||
|
||||
// Address the Accept To the interacting acct.
|
||||
ap.AppendTo(accept, interactingAcctURI)
|
||||
|
||||
// Send the Accept via the Actor's outbox.
|
||||
if _, err := f.FederatingActor().Send(
|
||||
ctx, outboxIRI, accept,
|
||||
); err != nil {
|
||||
return gtserror.Newf(
|
||||
"error sending activity %T for %v via outbox %s: %w",
|
||||
accept, approval.InteractionType, outboxIRI, err,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -135,6 +135,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
// ACCEPT USER (ie., new user+account sign-up)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.AcceptUser(ctx, cMsg)
|
||||
|
||||
// ACCEPT NOTE/STATUS (ie., accept a reply)
|
||||
case ap.ObjectNote:
|
||||
return p.clientAPI.AcceptReply(ctx, cMsg)
|
||||
|
||||
// ACCEPT LIKE
|
||||
case ap.ActivityLike:
|
||||
return p.clientAPI.AcceptLike(ctx, cMsg)
|
||||
|
||||
// ACCEPT BOOST
|
||||
case ap.ActivityAnnounce:
|
||||
return p.clientAPI.AcceptAnnounce(ctx, cMsg)
|
||||
}
|
||||
|
||||
// REJECT SOMETHING
|
||||
@ -236,6 +248,61 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// If pending approval is true then status must
|
||||
// reply to a status (either one of ours or a
|
||||
// remote) that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
status.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then send out the Create to
|
||||
// only the replied-to account (if it's remote),
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
@ -362,6 +429,61 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
||||
return gtserror.Newf("error populating status fave: %w", err)
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then send out the Like to
|
||||
// only the faved account (if it's remote),
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
@ -383,6 +505,61 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then send out the Announce to
|
||||
// only the boosted account (if it's remote),
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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 early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and boost is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal,
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the actor account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
@ -874,3 +1051,18 @@ func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// TODO
|
||||
return nil
|
||||
}
|
||||
|
@ -122,11 +122,23 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
||||
|
||||
// ACCEPT SOMETHING
|
||||
case ap.ActivityAccept:
|
||||
switch fMsg.APObjectType { //nolint:gocritic
|
||||
switch fMsg.APObjectType {
|
||||
|
||||
// ACCEPT FOLLOW
|
||||
// ACCEPT (pending) FOLLOW
|
||||
case ap.ActivityFollow:
|
||||
return p.fediAPI.AcceptFollow(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) LIKE
|
||||
case ap.ActivityLike:
|
||||
return p.fediAPI.AcceptLike(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) REPLY
|
||||
case ap.ObjectNote:
|
||||
return p.fediAPI.AcceptReply(ctx, fMsg)
|
||||
|
||||
// ACCEPT (pending) ANNOUNCE
|
||||
case ap.ActivityAnnounce:
|
||||
return p.fediAPI.AcceptAnnounce(ctx, fMsg)
|
||||
}
|
||||
|
||||
// DELETE SOMETHING
|
||||
@ -216,6 +228,52 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// status must reply to a LOCAL status
|
||||
// that requires approval for the reply.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
status.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !status.PreApproved:
|
||||
// If approval is required and status isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: 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)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && status.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a reply
|
||||
// to one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the remote account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, status); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
@ -348,6 +406,52 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
||||
return gtserror.Newf("error populating status fave: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// fave must target a LOCAL status
|
||||
// that requires approval for the fave.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
fave.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !fave.PreApproved:
|
||||
// If approval is required and fave isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: they can
|
||||
// approve or deny the interaction later.
|
||||
|
||||
// Notify *local* account of pending fave.
|
||||
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying pending fave: %v", err)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && fave.PreApproved:
|
||||
// If approval is required and fave is
|
||||
// preapproved, that means this is a fave
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||
log.Errorf(ctx, "error notifying fave: %v", err)
|
||||
}
|
||||
@ -365,8 +469,9 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
|
||||
}
|
||||
|
||||
// Dereference status that this boosts, note
|
||||
// that this will handle storing the boost in
|
||||
// Dereference into a boost wrapper status.
|
||||
//
|
||||
// Note: this will handle storing the boost in
|
||||
// the db, and dereferencing the target status
|
||||
// ancestors / descendants where appropriate.
|
||||
var err error
|
||||
@ -376,8 +481,10 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||
fMsg.Receiving.Username,
|
||||
)
|
||||
if err != nil {
|
||||
if gtserror.IsUnretrievable(err) {
|
||||
// Boosted status domain blocked, nothing to do.
|
||||
if gtserror.IsUnretrievable(err) ||
|
||||
gtserror.NotPermitted(err) {
|
||||
// Boosted status domain blocked, or
|
||||
// otherwise not permitted, nothing to do.
|
||||
log.Debugf(ctx, "skipping announce: %v", err)
|
||||
return nil
|
||||
}
|
||||
@ -386,6 +493,52 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||
return gtserror.Newf("error dereferencing announce: %w", err)
|
||||
}
|
||||
|
||||
// If pending approval is true then
|
||||
// boost must target a LOCAL status
|
||||
// that requires approval for the boost.
|
||||
pendingApproval := util.PtrOrValue(
|
||||
boost.PendingApproval,
|
||||
false,
|
||||
)
|
||||
|
||||
switch {
|
||||
case pendingApproval && !boost.PreApproved:
|
||||
// If approval is required and boost isn't
|
||||
// preapproved, then just notify the account
|
||||
// that's being interacted with: 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)
|
||||
}
|
||||
|
||||
// Return early.
|
||||
return nil
|
||||
|
||||
case pendingApproval && boost.PreApproved:
|
||||
// If approval is required and status is
|
||||
// preapproved, that means this is a boost
|
||||
// of one of our statuses with permission
|
||||
// that matched on a following/followers
|
||||
// collection. Do the Accept immediately and
|
||||
// then process everything else as normal.
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Don't return, just continue as normal.
|
||||
}
|
||||
|
||||
// Update stats for the remote account.
|
||||
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
@ -549,6 +702,68 @@ func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
// TODO: Add something here if we ever implement sending out Likes to
|
||||
// followers more broadly and not just the owner of the Liked status.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
status, 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 {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
}
|
||||
|
||||
// Timeline and notify the status.
|
||||
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error timelining and notifying status: %v", err)
|
||||
}
|
||||
|
||||
// Interaction counts changed on the replied-to status;
|
||||
// uncache the prepared version from all timelines.
|
||||
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
|
||||
|
||||
// Send out the reply again, fully this time.
|
||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
boost, 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, boost.Account, boost); err != nil {
|
||||
log.Errorf(ctx, "error updating account stats: %v", err)
|
||||
}
|
||||
|
||||
// Timeline and notify the boost wrapper status.
|
||||
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(ctx, boost.BoostOfID)
|
||||
|
||||
// Send out the boost again, fully this time.
|
||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||
log.Errorf(ctx, "error federating announce: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
|
||||
// Cast the existing Status model attached to msg.
|
||||
existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
|
||||
|
@ -32,6 +32,62 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// notifyPendingReply notifies the account replied-to
|
||||
// by the given status that they have a new reply,
|
||||
// and that approval is pending.
|
||||
func (s *Surface) notifyPendingReply(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) error {
|
||||
// Beforehand, ensure the passed status is fully populated.
|
||||
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
if status.InReplyToAccount.IsRemote() {
|
||||
// Don't notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
}
|
||||
|
||||
if status.AccountID == status.InReplyToAccountID {
|
||||
// Don't notify
|
||||
// self-replies.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure thread not muted
|
||||
// by replied-to account.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
status.ThreadID,
|
||||
status.InReplyToAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", status.ThreadID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// The replied-to account
|
||||
// has muted the thread.
|
||||
// Don't pester them.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify mentioned
|
||||
// by status author.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingReply,
|
||||
status.InReplyToAccount,
|
||||
status.Account,
|
||||
status.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying replied-to account %s: %w", status.InReplyToAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyMentions iterates through mentions on the
|
||||
// given status, and notifies each mentioned account
|
||||
// that they have a new mention.
|
||||
@ -181,36 +237,13 @@ func (s *Surface) notifyFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) error {
|
||||
if fave.TargetAccountID == fave.AccountID {
|
||||
// Self-fave, nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status fave is fully populated.
|
||||
if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
return gtserror.Newf("error populating fave %s: %w", fave.ID, err)
|
||||
}
|
||||
|
||||
if fave.TargetAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure favee hasn't
|
||||
// muted the thread.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
fave.Status.ThreadID,
|
||||
fave.TargetAccountID,
|
||||
)
|
||||
notifyable, err := s.notifyableFave(ctx, fave)
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Favee doesn't want
|
||||
// notifs for this thread.
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -228,31 +261,167 @@ func (s *Surface) notifyFave(
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyPendingFave notifies the target of the
|
||||
// given fave that their status has been faved
|
||||
// and that approval is required.
|
||||
func (s *Surface) notifyPendingFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) error {
|
||||
notifyable, err := s.notifyableFave(ctx, fave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of fave by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingFave,
|
||||
fave.TargetAccount,
|
||||
fave.Account,
|
||||
fave.StatusID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyableFave checks that the given
|
||||
// fave should be notified, taking account
|
||||
// of localness of receiving account, and mutes.
|
||||
func (s *Surface) notifyableFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) (bool, error) {
|
||||
if fave.TargetAccountID == fave.AccountID {
|
||||
// Self-fave, nothing to do.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status fave is fully populated.
|
||||
if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||
return false, gtserror.Newf("error populating fave %s: %w", fave.ID, err)
|
||||
}
|
||||
|
||||
if fave.TargetAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Ensure favee hasn't
|
||||
// muted the thread.
|
||||
muted, err := s.State.DB.IsThreadMutedByAccount(
|
||||
ctx,
|
||||
fave.Status.ThreadID,
|
||||
fave.TargetAccountID,
|
||||
)
|
||||
if err != nil {
|
||||
return false, gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Favee doesn't want
|
||||
// notifs for this thread.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// notifyAnnounce notifies the status boost target
|
||||
// account that their status has been boosted.
|
||||
func (s *Surface) notifyAnnounce(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
boost *gtsmodel.Status,
|
||||
) error {
|
||||
notifyable, err := s.notifyableAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationReblog,
|
||||
boost.BoostOfAccount,
|
||||
boost.Account,
|
||||
boost.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyPendingAnnounce notifies the status boost
|
||||
// target account that their status has been boosted,
|
||||
// and that the boost requires approval.
|
||||
func (s *Surface) notifyPendingAnnounce(
|
||||
ctx context.Context,
|
||||
boost *gtsmodel.Status,
|
||||
) error {
|
||||
notifyable, err := s.notifyableAnnounce(ctx, boost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !notifyable {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationPendingReblog,
|
||||
boost.BoostOfAccount,
|
||||
boost.Account,
|
||||
boost.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// notifyableAnnounce checks that the given
|
||||
// announce should be notified, taking account
|
||||
// of localness of receiving account, and mutes.
|
||||
func (s *Surface) notifyableAnnounce(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) (bool, error) {
|
||||
if status.BoostOfID == "" {
|
||||
// Not a boost, nothing to do.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if status.BoostOfAccountID == status.AccountID {
|
||||
// Self-boost, nothing to do.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Beforehand, ensure the passed status is fully populated.
|
||||
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
|
||||
return gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
return false, gtserror.Newf("error populating status %s: %w", status.ID, err)
|
||||
}
|
||||
|
||||
if status.BoostOfAccount.IsRemote() {
|
||||
// no need to notify
|
||||
// remote accounts.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Ensure boostee hasn't
|
||||
@ -264,27 +433,16 @@ func (s *Surface) notifyAnnounce(
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err)
|
||||
return false, gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err)
|
||||
}
|
||||
|
||||
if muted {
|
||||
// Boostee doesn't want
|
||||
// notifs for this thread.
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// notify status author
|
||||
// of boost by account.
|
||||
if err := s.Notify(ctx,
|
||||
gtsmodel.NotificationReblog,
|
||||
status.BoostOfAccount,
|
||||
status.Account,
|
||||
status.ID,
|
||||
); err != nil {
|
||||
return gtserror.Newf("error notifying status author %s: %w", status.BoostOfAccountID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error {
|
||||
|
@ -26,10 +26,13 @@ import (
|
||||
"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/processing/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// util provides util functions used by both
|
||||
@ -498,3 +501,129 @@ func (u *utils) decrementFollowRequestsCount(
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// approveFave stores + returns an
|
||||
// interactionApproval for a fave.
|
||||
func (u *utils) approveFave(
|
||||
ctx context.Context,
|
||||
fave *gtsmodel.StatusFave,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: fave.TargetAccountID,
|
||||
Account: fave.TargetAccount,
|
||||
InteractingAccountID: fave.AccountID,
|
||||
InteractingAccount: fave.Account,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the fave itself as now approved.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.PreApproved = false
|
||||
fave.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating status fave: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
||||
// approveReply stores + returns an
|
||||
// interactionApproval for a reply.
|
||||
func (u *utils) approveReply(
|
||||
ctx context.Context,
|
||||
status *gtsmodel.Status,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: status.InReplyToAccountID,
|
||||
Account: status.InReplyToAccount,
|
||||
InteractingAccountID: status.AccountID,
|
||||
InteractingAccount: status.Account,
|
||||
InteractionURI: status.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the status itself as now approved.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.PreApproved = false
|
||||
status.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
status,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating status: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
||||
// approveAnnounce stores + returns an
|
||||
// interactionApproval for an announce.
|
||||
func (u *utils) approveAnnounce(
|
||||
ctx context.Context,
|
||||
boost *gtsmodel.Status,
|
||||
) (*gtsmodel.InteractionApproval, error) {
|
||||
id := id.NewULID()
|
||||
|
||||
approval := >smodel.InteractionApproval{
|
||||
ID: id,
|
||||
AccountID: boost.BoostOfAccountID,
|
||||
Account: boost.BoostOfAccount,
|
||||
InteractingAccountID: boost.AccountID,
|
||||
InteractingAccount: boost.Account,
|
||||
InteractionURI: boost.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
|
||||
}
|
||||
|
||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Mark the status itself as now approved.
|
||||
boost.PendingApproval = util.Ptr(false)
|
||||
boost.PreApproved = false
|
||||
boost.ApprovedByURI = approval.URI
|
||||
|
||||
if err := u.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
boost,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating boost wrapper status: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return approval, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user