diff --git a/internal/ap/activitystreams.go b/internal/ap/activitystreams.go index 56ebc4909..7c22fc65c 100644 --- a/internal/ap/activitystreams.go +++ b/internal/ap/activitystreams.go @@ -102,13 +102,23 @@ const ( /* GtS stuff */ - ObjectLikeApproval = "LikeApproval" - ObjectReplyApproval = "ReplyApproval" - ObjectAnnounceApproval = "AnnounceApproval" + ObjectLikeAuthorization = "LikeAuthorization" + ObjectReplyAuthorization = "ReplyAuthorization" + ObjectAnnounceAuthorization = "AnnounceAuthorization" + + ActivityLikeRequest = "LikeRequest" + ActivityReplyRequest = "ReplyRequest" + ActivityAnnounceRequest = "AnnounceRequest" /* Funkwhale stuff */ ObjectAlbum = "Album" + + /* Deprecated stuff */ + + ObjectLikeApproval = "LikeApproval" // deprecated, use LikeAuthorization. + ObjectReplyApproval = "ReplyApproval" // deprecated, use ReplyAuthorization. + ObjectAnnounceApproval = "AnnounceApproval" // deprecated, use AnnounceAuthorization. ) // isActivity returns whether AS type name is of an Activity (NOT IntransitiveActivity). diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go index 3de75ded1..f9f55f093 100644 --- a/internal/db/bundb/interaction.go +++ b/internal/db/bundb/interaction.go @@ -218,7 +218,7 @@ func (i *interactionDB) PopulateInteractionRequest(ctx context.Context, req *gts errs.Appendf("error populating interactionRequest Like: %w", err) } - case gtsmodel.InteractionReply: + case gtsmodel.InteractionReply, gtsmodel.InteractionReplyRequest: req.Reply, err = i.state.DB.GetStatusByURI(ctx, req.InteractionURI) if err != nil && !errors.Is(err, db.ErrNoEntries) { errs.Appendf("error populating interactionRequest Reply: %w", err) diff --git a/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat.go b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat.go new file mode 100644 index 000000000..210d4b2c5 --- /dev/null +++ b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat.go @@ -0,0 +1,42 @@ +// 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 . + +package migrations + +import ( + "context" + + "github.com/uptrace/bun" +) + +func init() { + up := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + down := func(ctx context.Context, db *bun.DB) error { + return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + return nil + }) + } + + if err := Migrations.Register(up, down); err != nil { + panic(err) + } +} diff --git a/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interaction.go b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interaction.go new file mode 100644 index 000000000..81586741f --- /dev/null +++ b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interaction.go @@ -0,0 +1,33 @@ +// 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 . + +package gtsmodel + +import "time" + +type InteractionRequest struct { + ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` + CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` + StatusID string `bun:"type:CHAR(26),nullzero,notnull"` + TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` + InteractionURI string `bun:",nullzero,notnull,unique"` + InteractionType int `bun:",notnull"` + AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` + RejectedAt time.Time `bun:"type:timestamptz,nullzero"` + ResponseURI string `bun:"uri,nullzero,unique"` +} diff --git a/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interactionpolicy.go b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interactionpolicy.go new file mode 100644 index 000000000..5972daa21 --- /dev/null +++ b/internal/db/bundb/migrations/20250510133509_interaction_policies_forward_compat/interactionpolicy.go @@ -0,0 +1,33 @@ +// 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 . + +package gtsmodel + +type PolicyValue string + +type PolicyValues []PolicyValue + +type InteractionPolicy struct { + CanLike PolicyRules + CanReply PolicyRules + CanAnnounce PolicyRules +} + +type PolicyRules struct { + AutomaticApproval PolicyValues `json:"Always,omitempty"` + ManualApproval PolicyValues `json:"WithApproval,omitempty"` +} diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go index 0c4d21c64..9169325b0 100644 --- a/internal/federation/federatingdb/db.go +++ b/internal/federation/federatingdb/db.go @@ -51,6 +51,11 @@ var _ interface { Move(context.Context, vocab.ActivityStreamsMove) error Flag(context.Context, vocab.ActivityStreamsFlag) error + // Custom types. + LikeRequest(context.Context, vocab.GoToSocialLikeRequest) error + ReplyRequest(context.Context, vocab.GoToSocialReplyRequest) error + AnnounceRequest(context.Context, vocab.GoToSocialAnnounceRequest) error + /* Extra/convenience functionality. */ diff --git a/internal/federation/federatingdb/interactionreqs.go b/internal/federation/federatingdb/interactionreqs.go new file mode 100644 index 000000000..a5c5f7d5d --- /dev/null +++ b/internal/federation/federatingdb/interactionreqs.go @@ -0,0 +1,152 @@ +// 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 . + +package federatingdb + +import ( + "context" + "net/http" + "time" + + "code.superseriousbusiness.org/activity/streams/vocab" + "code.superseriousbusiness.org/gotosocial/internal/ap" + "code.superseriousbusiness.org/gotosocial/internal/gtserror" + "code.superseriousbusiness.org/gotosocial/internal/gtsmodel" + "code.superseriousbusiness.org/gotosocial/internal/id" + "code.superseriousbusiness.org/gotosocial/internal/log" + "code.superseriousbusiness.org/gotosocial/internal/messages" +) + +func (f *DB) LikeRequest( + ctx context.Context, + likeReq vocab.GoToSocialLikeRequest, +) error { + log.DebugKV(ctx, "LikeRequest", serialize{likeReq}) + + // Mark activity as handled. + f.storeActivityID(likeReq) + + // Extract relevant values from passed ctx. + activityContext := getActivityContext(ctx) + if activityContext.internal { + return nil // Already processed. + } + + requesting := activityContext.requestingAcct + receiving := activityContext.receivingAcct + + if receiving.IsMoving() { + // A Moving account + // can't accept a Like. + return nil + } + + // Convert received LikeRequest type to dummy + // fave, so that we can check against policies. + // This dummy won't be stored in the database, + // it's used purely for doing permission checks. + dummyFave, err := f.converter.ASLikeToFave(ctx, likeReq) + if err != nil { + err := gtserror.Newf("error converting from AS type: %w", err) + return gtserror.WrapWithCode(http.StatusBadRequest, err) + } + + if !*dummyFave.Status.Local { + // Only process like requests for local statuses. + // + // If the remote has sent us a like request for a + // status that's not ours, we should ignore it. + return nil + } + + // Ensure fave would be enacted by correct account. + if dummyFave.AccountID != requesting.ID { + return gtserror.NewfWithCode(http.StatusForbidden, "requester %s is not expected actor %s", + requesting.URI, dummyFave.Account.URI) + } + + // Ensure fave would be received by correct account. + if dummyFave.TargetAccountID != receiving.ID { + return gtserror.NewfWithCode(http.StatusForbidden, "receiver %s is not expected object %s", + receiving.URI, dummyFave.TargetAccount.URI) + } + + // Check how we should handle this request. + policyResult, err := f.intFilter.StatusLikeable(ctx, + requesting, + dummyFave.Status, + ) + if err != nil { + return gtserror.Newf("error seeing if status %s is likeable: %w", dummyFave.Status.URI, err) + } + + // Determine whether to automatically accept, + // automatically reject, or pend approval. + var ( + acceptedAt time.Time + rejectedAt time.Time + ) + if policyResult.AutomaticApproval() { + acceptedAt = time.Now() + } else if policyResult.Forbidden() { + rejectedAt = time.Now() + } + + interactionReq := >smodel.InteractionRequest{ + ID: id.NewULID(), + StatusID: dummyFave.Status.ID, + Status: dummyFave.Status, + TargetAccountID: receiving.ID, + TargetAccount: receiving, + InteractingAccountID: requesting.ID, + InteractingAccount: requesting, + InteractionURI: dummyFave.URI, + InteractionType: gtsmodel.InteractionLikeRequest, + AcceptedAt: acceptedAt, + RejectedAt: rejectedAt, + + // Empty as reject/accept + // response not yet sent. + URI: "", + } + + // Send the interactionReq through to + // the processor to handle side effects. + f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{ + APObjectType: ap.ActivityLikeRequest, + APActivityType: ap.ActivityCreate, + GTSModel: interactionReq, + Receiving: receiving, + Requesting: requesting, + }) + + return nil +} + +func (f *DB) ReplyRequest( + ctx context.Context, + replyReq vocab.GoToSocialReplyRequest, +) error { + return nil +} + +func (f *DB) AnnounceRequest( + ctx context.Context, + announceReq vocab.GoToSocialAnnounceRequest, +) error { + return nil +} diff --git a/internal/federation/federator.go b/internal/federation/federator.go index 5f8324da2..3f5c96002 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -99,6 +99,9 @@ func NewFederator( federatingDB.Announce, federatingDB.Move, federatingDB.Flag, + federatingDB.LikeRequest, + federatingDB.ReplyRequest, + federatingDB.AnnounceRequest, }, } actor := newFederatingActor(f, f, federatingDB, clock) diff --git a/internal/gtsmodel/interaction.go b/internal/gtsmodel/interaction.go index 0b9ee693e..d1dd8ba95 100644 --- a/internal/gtsmodel/interaction.go +++ b/internal/gtsmodel/interaction.go @@ -29,22 +29,47 @@ const ( // If you need to add new interaction types, // add them *to the end* of the list. + /* + Types for when a requester straight up + sends a Like or a Create or an Announce. + + In this case we will have the Like or the + Reply or the Announce stored in the db + marked as pending or whatever. + */ + InteractionLike InteractionType = iota InteractionReply InteractionAnnounce + + /* + Types for when a requester politely sends + an XyzRequest asking for permission first. + + In this case we don't store a Like or an + Announce in the db, as they don't exist yet, + but we do store a Reply if reply is pending + approval (for review) or approved, as the + proposed reply should have been sent along + as the object of the ReplyRequest. + */ + + InteractionLikeRequest + InteractionReplyRequest + InteractionAnnounceRequest ) // Stringifies this InteractionType in a // manner suitable for serving via the API. func (i InteractionType) String() string { switch i { - case InteractionLike: + case InteractionLike, InteractionLikeRequest: const text = "favourite" return text - case InteractionReply: + case InteractionReply, InteractionReplyRequest: const text = "reply" return text - case InteractionAnnounce: + case InteractionAnnounce, InteractionAnnounceRequest: const text = "reblog" return text default: @@ -64,10 +89,10 @@ type InteractionRequest struct { TargetAccount *Account `bun:"-"` // Not stored in DB. Account being interacted with. InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account requesting the interaction. InteractingAccount *Account `bun:"-"` // Not stored in DB. Account corresponding to targetAccountID - InteractionURI string `bun:",nullzero,notnull,unique"` // URI of the interacting like, reply, or announce. Unique (only one interaction request allowed per interaction URI). - InteractionType InteractionType `bun:",notnull"` // One of Like, Reply, or Announce. + InteractionURI string `bun:",nullzero,notnull,unique"` // URI of the interacting like (request), reply (request), or announce (request). Unique (only one interaction request allowed per interaction URI). + InteractionType InteractionType `bun:",notnull"` // One of Like, LikeRequest, Reply, ReplyRequest, or Announce, AnnounceRequest. Like *StatusFave `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionLike. - Reply *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionReply. + Reply *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionReply or InteractionType = InteractionReplyRequest. Announce *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionAnnounce. AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was accepted, time at which this occurred. RejectedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was rejected, time at which this occurred. diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index 6fe4ed821..99882e6f4 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -19,7 +19,8 @@ package gtsmodel import "time" -// StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account +// StatusFave refers to a 'fave' or 'like' in the database, +// from one account, targeting the status of another account type StatusFave struct { ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created diff --git a/internal/processing/interactionrequests/accept.go b/internal/processing/interactionrequests/accept.go index ce682380b..8d6f9ddd7 100644 --- a/internal/processing/interactionrequests/accept.go +++ b/internal/processing/interactionrequests/accept.go @@ -203,26 +203,23 @@ func (p *Processor) acceptAnnounce( ctx context.Context, req *gtsmodel.InteractionRequest, ) gtserror.WithCode { - // If the Announce is missing, that means it's - // probably already been undone by someone, - // so there's nothing to actually accept. - if req.Reply == nil { - err := gtserror.Newf("no Announce found for interaction request %s", req.ID) - return gtserror.NewErrorNotFound(err) - } - - // Update the Announce. - req.Announce.PendingApproval = util.Ptr(false) - req.Announce.PreApproved = false - req.Announce.ApprovedByURI = req.URI - if err := p.state.DB.UpdateStatus( - ctx, - req.Announce, - "pending_approval", - "approved_by_uri", - ); err != nil { - err := gtserror.Newf("db error updating status announce: %w", err) - return gtserror.NewErrorInternalError(err) + // If the Announce is set, that means it comes + // from someone straight up sending the Announce + // instead of AnnounceRequest, so we already have + // the Announce in the db. We can update it now. + if req.Announce != nil { + req.Announce.PendingApproval = util.Ptr(false) + req.Announce.PreApproved = false + req.Announce.ApprovedByURI = req.URI + if err := p.state.DB.UpdateStatus( + ctx, + req.Announce, + "pending_approval", + "approved_by_uri", + ); err != nil { + err := gtserror.Newf("db error updating status announce: %w", err) + return gtserror.NewErrorInternalError(err) + } } // Send the accepted request off through the diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go index 5d9ebf41a..fbd24a423 100644 --- a/internal/processing/workers/fromclientapi.go +++ b/internal/processing/workers/fromclientapi.go @@ -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.replyToRequestReply(ctx, status); err != nil { return gtserror.Newf("error pending reply: %w", err) } @@ -494,7 +494,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.faveToPendingFave(ctx, fave); err != nil { return gtserror.Newf("error pending fave: %w", err) } @@ -555,7 +555,11 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI // Don't return, just continue as normal. } - if err := p.surface.notifyFave(ctx, fave); err != nil { + if err := p.surface.notifyFave(ctx, + fave.Account, + fave.TargetAccount, + fave.Status, + ); err != nil { log.Errorf(ctx, "error notifying fave: %v", err) } @@ -589,7 +593,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.announceToRequestAnnounce(ctx, boost); err != nil { return gtserror.Newf("error pending boost: %w", err) } @@ -661,7 +665,11 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien } // Notify the boost target account. - if err := p.surface.notifyAnnounce(ctx, boost); err != nil { + if err := p.surface.notifyAnnounce(ctx, + boost.Account, + boost.BoostOfAccount, + boost.BoostOf, + ); err != nil { log.Errorf(ctx, "error notifying boost: %v", err) } @@ -1217,8 +1225,13 @@ func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI 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 { + // Notify the fave (distinct from + // the notif for the pending fave). + if err := p.surface.notifyFave(ctx, + req.InteractingAccount, + req.TargetAccount, + req.Status, + ); err != nil { log.Errorf(ctx, "error notifying fave: %v", err) } @@ -1273,6 +1286,25 @@ func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClien return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel) } + // Send out the Accept. + if err := p.federate.AcceptInteraction(ctx, req); err != nil { + log.Errorf(ctx, "error federating approval of announce: %v", err) + } + + // If req.Announce is not set, that means we got + // the request politely as AnnounceRequest, and + // so we don't have the Announce wrapper stored + // because it hasn't been sent yet. + if req.Announce == nil { + // Nothing to do but wait for the + // remote to send the Announce. + return nil + } + + // If it *is* set, that means it comes from someone + // straight up sending the Announce first instead of + // AnnounceRequest, so we already have the Announce + // in the db, and we can process it now. var ( interactingAcct = req.InteractingAccount boost = req.Announce @@ -1288,16 +1320,17 @@ func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClien 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 { + // Notify the announce (distinct from + // the notif for the pending announce). + if err := p.surface.notifyAnnounce( + ctx, + boost.Account, + boost.BoostOfAccount, + boost.BoostOf, + ); 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(boost.BoostOfID) diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go index d1e5bb2f7..4017e29fd 100644 --- a/internal/processing/workers/fromfediapi.go +++ b/internal/processing/workers/fromfediapi.go @@ -96,6 +96,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF case ap.ActivityLike: return p.fediAPI.CreateLike(ctx, fMsg) + // CREATE LIKE/FAVE REQUEST + case ap.ActivityLikeRequest: + return p.fediAPI.CreateLikeRequest(ctx, fMsg) + // CREATE ANNOUNCE/BOOST case ap.ActivityAnnounce: return p.fediAPI.CreateAnnounce(ctx, fMsg) @@ -291,7 +295,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.replyToRequestReply(ctx, status); err != nil { return gtserror.Newf("error pending reply: %w", err) } @@ -525,7 +529,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.faveToPendingFave(ctx, fave); err != nil { return gtserror.Newf("error pending fave: %w", err) } @@ -580,7 +584,11 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er // Don't return, just continue as normal. } - if err := p.surface.notifyFave(ctx, fave); err != nil { + if err := p.surface.notifyFave(ctx, + fave.Account, + fave.TargetAccount, + fave.Status, + ); err != nil { log.Errorf(ctx, "error notifying fave: %v", err) } @@ -591,6 +599,76 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er return nil } +func (p *fediAPI) CreateLikeRequest( + ctx context.Context, + fMsg *messages.FromFediAPI, +) error { + // Unlike InteractionReq from xyz, InteractionReq from + // xyzRequest will only ever have xyz set on it if xyz + // is a reply, not a Like or an Announce. + // + // In this case, that means there's no Fave set on it. + interactionReq, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) + } + + // Whatever happens, we'll need to store the request. + // + // AcceptedAt or RejectedAt should already be set on the + // request if it's automatically accepted, or not permitted, + // so we don't have to do that here. + err := p.state.DB.PutInteractionRequest(ctx, interactionReq) + switch { + case err == nil: + // All good, + // it's stored. + + case errors.Is(err, db.ErrAlreadyExists): + // Request already stored, + // did something race? + // Nothing to in that case. + return nil + + default: + // Real error, cannot continue. + return gtserror.Newf("db error storing like request: %w", err) + } + + // Process side effects. + switch { + case interactionReq.IsPending(): + // If pending, ie., manual approval + // required, the only side effect is + // to notify the interactee. + if err := p.utils.surface.notifyPendingFave( + ctx, + interactionReq.InteractingAccount, + interactionReq.TargetAccount, + interactionReq.Status, + ); err != nil { + log.Errorf(ctx, "error storing pending like notif: %v", err) + } + + case interactionReq.IsAccepted(): + // If accepted, ie., automatic approval, + // just send out the Accept message and + // wait for the Like to be delivered. + if err := p.federate.AcceptInteraction(ctx, interactionReq); err != nil { + log.Errorf(ctx, "error sending like accept: %v", err) + } + + case interactionReq.IsRejected(): + // If rejected, ie., not permitted, + // just send out the Reject message. + if err := p.federate.RejectInteraction(ctx, interactionReq); err != nil { + log.Errorf(ctx, "error sending like reject: %v", err) + } + } + + return nil +} + func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error { boost, ok := fMsg.GTSModel.(*gtsmodel.Status) if !ok { @@ -632,7 +710,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.announceToRequestAnnounce(ctx, boost); err != nil { return gtserror.Newf("error pending boost: %w", err) } @@ -697,7 +775,12 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI log.Errorf(ctx, "error timelining and notifying status: %v", err) } - if err := p.surface.notifyAnnounce(ctx, boost); err != nil { + if err := p.surface.notifyAnnounce( + ctx, + boost.Account, + boost.BoostOfAccount, + boost.BoostOf, + ); err != nil { log.Errorf(ctx, "error notifying announce: %v", err) } @@ -708,6 +791,76 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI return nil } +func (p *fediAPI) CreateAnnounceRequest( + ctx context.Context, + fMsg *messages.FromFediAPI, +) error { + // Unlike InteractionReq from xyz, InteractionReq from + // xyzRequest will only ever have xyz set on it if xyz + // is a reply, not a Like or an Announce. + // + // In this case, that means there's no Announce set on it. + interactionReq, ok := fMsg.GTSModel.(*gtsmodel.InteractionRequest) + if !ok { + return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", fMsg.GTSModel) + } + + // Whatever happens, we'll need to store the request. + // + // AcceptedAt or RejectedAt should already be set on the + // request if it's automatically accepted, or not permitted, + // so we don't have to do that here. + err := p.state.DB.PutInteractionRequest(ctx, interactionReq) + switch { + case err == nil: + // All good, + // it's stored. + + case errors.Is(err, db.ErrAlreadyExists): + // Request already stored, + // did something race? + // Nothing to in that case. + return nil + + default: + // Real error, cannot continue. + return gtserror.Newf("db error storing announce request: %w", err) + } + + // Process side effects. + switch { + case interactionReq.IsPending(): + // If pending, ie., manual approval + // required, the only side effect is + // to notify the interactee. + if err := p.utils.surface.notifyPendingAnnounce( + ctx, + interactionReq.InteractingAccount, + interactionReq.TargetAccount, + interactionReq.Status, + ); err != nil { + log.Errorf(ctx, "error storing pending announce notif: %v", err) + } + + case interactionReq.IsAccepted(): + // If accepted, ie., automatic approval, + // just send out the Accept message and + // wait for the Announce to be delivered. + if err := p.federate.AcceptInteraction(ctx, interactionReq); err != nil { + log.Errorf(ctx, "error sending announce accept: %v", err) + } + + case interactionReq.IsRejected(): + // If rejected, ie., not permitted, + // just send out the Reject message. + if err := p.federate.RejectInteraction(ctx, interactionReq); err != nil { + log.Errorf(ctx, "error sending announce reject: %v", err) + } + } + + return nil +} + func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error { block, ok := fMsg.GTSModel.(*gtsmodel.Block) if !ok { diff --git a/internal/processing/workers/surfacenotify.go b/internal/processing/workers/surfacenotify.go index 11c3fd059..e638a3842 100644 --- a/internal/processing/workers/surfacenotify.go +++ b/internal/processing/workers/surfacenotify.go @@ -253,13 +253,19 @@ func (s *Surface) notifyFollow( return nil } -// notifyFave notifies the target of the given -// fave that their status has been liked/faved. +// notifyFave notifies the target of of a +// fave that their status has been faved. func (s *Surface) notifyFave( ctx context.Context, - fave *gtsmodel.StatusFave, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + status *gtsmodel.Status, ) error { - notifyable, err := s.notifyableFave(ctx, fave) + notifyable, err := s.notifyableFave(ctx, + account, + targetAccount, + status, + ) if err != nil { return err } @@ -273,24 +279,30 @@ func (s *Surface) notifyFave( // of fave by account. if err := s.Notify(ctx, gtsmodel.NotificationFavourite, - fave.TargetAccount, - fave.Account, - fave.StatusID, + targetAccount, + account, + status.ID, ); err != nil { - return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) + return gtserror.Newf("error notifying status author %s: %w", targetAccount.ID, err) } return nil } -// notifyPendingFave notifies the target of the -// given fave that their status has been faved -// and that approval is required. +// notifyPendingFave notifies the target of a +// fave that their status has been faved and +// that approval is required. func (s *Surface) notifyPendingFave( ctx context.Context, - fave *gtsmodel.StatusFave, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + status *gtsmodel.Status, ) error { - notifyable, err := s.notifyableFave(ctx, fave) + notifyable, err := s.notifyableFave(ctx, + account, + targetAccount, + status, + ) if err != nil { return err } @@ -304,34 +316,31 @@ func (s *Surface) notifyPendingFave( // of fave by account. if err := s.Notify(ctx, gtsmodel.NotificationPendingFave, - fave.TargetAccount, - fave.Account, - fave.StatusID, + targetAccount, + account, + status.ID, ); err != nil { - return gtserror.Newf("error notifying status author %s: %w", fave.TargetAccountID, err) + return gtserror.Newf("error notifying status author %s: %w", targetAccount.ID, err) } return nil } -// notifyableFave checks that the given -// fave should be notified, taking account -// of localness of receiving account, and mutes. +// notifyableFave checks if a fave should +// be notified, taking account of localness +// of target account, and thread mutes. func (s *Surface) notifyableFave( ctx context.Context, - fave *gtsmodel.StatusFave, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + status *gtsmodel.Status, ) (bool, error) { - if fave.TargetAccountID == fave.AccountID { + if targetAccount.ID == account.ID { // 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() { + if targetAccount.IsRemote() { // no need to notify // remote accounts. return false, nil @@ -341,11 +350,11 @@ func (s *Surface) notifyableFave( // muted the thread. muted, err := s.State.DB.IsThreadMutedByAccount( ctx, - fave.Status.ThreadID, - fave.TargetAccountID, + status.ThreadID, + targetAccount.ID, ) if err != nil { - return false, gtserror.Newf("error checking status thread mute %s: %w", fave.StatusID, err) + return false, gtserror.Newf("error checking status thread mute %s: %w", status.ID, err) } if muted { @@ -357,13 +366,20 @@ func (s *Surface) notifyableFave( return true, nil } -// notifyAnnounce notifies the status boost target -// account that their status has been boosted. +// notifyAnnounce notifies the target +// acct that their status has been boosted. func (s *Surface) notifyAnnounce( ctx context.Context, - boost *gtsmodel.Status, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + targetStatus *gtsmodel.Status, ) error { - notifyable, err := s.notifyableAnnounce(ctx, boost) + notifyable, err := s.notifyableAnnounce( + ctx, + account, + targetAccount, + targetStatus, + ) if err != nil { return err } @@ -377,11 +393,11 @@ func (s *Surface) notifyAnnounce( // of boost by account. if err := s.Notify(ctx, gtsmodel.NotificationReblog, - boost.BoostOfAccount, - boost.Account, - boost.ID, + targetAccount, + account, + targetStatus.ID, ); err != nil { - return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) + return gtserror.Newf("error notifying boost target %s: %w", targetAccount.ID, err) } return nil @@ -392,9 +408,16 @@ func (s *Surface) notifyAnnounce( // and that the boost requires approval. func (s *Surface) notifyPendingAnnounce( ctx context.Context, - boost *gtsmodel.Status, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + targetStatus *gtsmodel.Status, ) error { - notifyable, err := s.notifyableAnnounce(ctx, boost) + notifyable, err := s.notifyableAnnounce( + ctx, + account, + targetAccount, + targetStatus, + ) if err != nil { return err } @@ -408,11 +431,11 @@ func (s *Surface) notifyPendingAnnounce( // of boost by account. if err := s.Notify(ctx, gtsmodel.NotificationPendingReblog, - boost.BoostOfAccount, - boost.Account, - boost.ID, + targetAccount, + account, + targetStatus.ID, ); err != nil { - return gtserror.Newf("error notifying boost target %s: %w", boost.BoostOfAccountID, err) + return gtserror.Newf("error notifying pending boost target %s: %w", targetAccount.ID, err) } return nil @@ -423,24 +446,21 @@ func (s *Surface) notifyPendingAnnounce( // of localness of receiving account, and mutes. func (s *Surface) notifyableAnnounce( ctx context.Context, - status *gtsmodel.Status, + account *gtsmodel.Account, + targetAccount *gtsmodel.Account, + targetStatus *gtsmodel.Status, ) (bool, error) { - if status.BoostOfID == "" { - // Not a boost, nothing to do. - return false, nil - } - - if status.BoostOfAccountID == status.AccountID { + if account.ID == targetStatus.AccountID { // Self-boost, nothing to do. return false, nil } - // Beforehand, ensure the passed status is fully populated. - if err := s.State.DB.PopulateStatus(ctx, status); err != nil { - return false, gtserror.Newf("error populating status %s: %w", status.ID, err) + // Ensure boosted status is populated. + if err := s.State.DB.PopulateStatus(ctx, targetStatus); err != nil { + return false, gtserror.Newf("error populating status %s: %w", targetStatus.ID, err) } - if status.BoostOfAccount.IsRemote() { + if targetStatus.Account.IsRemote() { // no need to notify // remote accounts. return false, nil @@ -450,12 +470,11 @@ func (s *Surface) notifyableAnnounce( // muted the thread. muted, err := s.State.DB.IsThreadMutedByAccount( ctx, - status.BoostOf.ThreadID, - status.BoostOfAccountID, + targetStatus.ThreadID, + targetAccount.ID, ) - if err != nil { - return false, gtserror.Newf("error checking status thread mute %s: %w", status.BoostOfID, err) + return false, gtserror.Newf("error checking status thread mute %s: %w", targetStatus.ID, err) } if muted { diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go index b9787456a..78eb9d170 100644 --- a/internal/processing/workers/util.go +++ b/internal/processing/workers/util.go @@ -526,9 +526,14 @@ func (u *utils) decrementFollowRequestsCount( return nil } -// requestFave stores an interaction request +// faveToPendingFave stores an interaction request // for the given fave, and notifies the interactee. -func (u *utils) requestFave( +// +// This is useful when a local account needs to send +// out a LikeRequest, or when a remote account has +// sent us a Like instead of a LikeRequest for a +// status where Liking requires approval. +func (u *utils) faveToPendingFave( ctx context.Context, fave *gtsmodel.StatusFave, ) error { @@ -561,17 +566,26 @@ func (u *utils) requestFave( return gtserror.Newf("db error storing interaction request: %w", err) } - // Notify *local* account of pending announce. - if err := u.surface.notifyPendingFave(ctx, fave); err != nil { + // Notify *local* account of pending fave. + if err := u.surface.notifyPendingFave(ctx, + fave.Account, + fave.TargetAccount, + fave.Status, + ); err != nil { return gtserror.Newf("error notifying pending fave: %w", err) } return nil } -// requestReply stores an interaction request +// replyToRequestReply stores an interaction request // for the given reply, and notifies the interactee. -func (u *utils) requestReply( +// +// This is useful when a local account needs to send +// out a ReplyRequest, or when a remote account has +// sent us a Create instead of a ReplyRequest for a +// status where replying requires approval. +func (u *utils) replyToRequestReply( ctx context.Context, reply *gtsmodel.Status, ) error { @@ -612,9 +626,14 @@ func (u *utils) requestReply( return nil } -// requestAnnounce stores an interaction request +// announceToRequestAnnounce stores an interaction request // for the given announce, and notifies the interactee. -func (u *utils) requestAnnounce( +// +// This is useful when a local account needs to send +// out an AnnounceRequest, or when a remote account has +// sent us an Announce instead of an AnnounceRequest for +// a status where announcing requires approval. +func (u *utils) announceToRequestAnnounce( ctx context.Context, boost *gtsmodel.Status, ) error { @@ -648,7 +667,12 @@ func (u *utils) requestAnnounce( } // Notify *local* account of pending announce. - if err := u.surface.notifyPendingAnnounce(ctx, boost); err != nil { + if err := u.surface.notifyPendingAnnounce( + ctx, + boost.Account, + boost.BoostOfAccount, + boost.BoostOf, + ); err != nil { return gtserror.Newf("error notifying pending announce: %w", err) }