mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 02:52:26 -06:00 
			
		
		
		
	
		
			
	
	
		
			573 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
		
		
			
		
	
	
			573 lines
		
	
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 
								 | 
							
								// 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 federatingdb
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								import (
							 | 
						||
| 
								 | 
							
									"context"
							 | 
						||
| 
								 | 
							
									"errors"
							 | 
						||
| 
								 | 
							
									"net/http"
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									"code.superseriousbusiness.org/activity/streams/vocab"
							 | 
						||
| 
								 | 
							
									"code.superseriousbusiness.org/gotosocial/internal/ap"
							 | 
						||
| 
								 | 
							
									"code.superseriousbusiness.org/gotosocial/internal/db"
							 | 
						||
| 
								 | 
							
									"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"
							 | 
						||
| 
								 | 
							
									"code.superseriousbusiness.org/gotosocial/internal/util"
							 | 
						||
| 
								 | 
							
								)
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								/*
							 | 
						||
| 
								 | 
							
									The code in this file handles the three types of "polite"
							 | 
						||
| 
								 | 
							
									interaction requests currently recognized by GoToSocial:
							 | 
						||
| 
								 | 
							
									LikeRequest, ReplyRequest, and AnnounceRequest.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									A request looks a bit like this, note the requested
							 | 
						||
| 
								 | 
							
									interaction itself is nested in the "instrument" property:
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									{
							 | 
						||
| 
								 | 
							
									  "@context": [
							 | 
						||
| 
								 | 
							
									    "https://www.w3.org/ns/activitystreams",
							 | 
						||
| 
								 | 
							
									    "https://gotosocial.org/ns"
							 | 
						||
| 
								 | 
							
									  ],
							 | 
						||
| 
								 | 
							
									  "type": "LikeRequest",
							 | 
						||
| 
								 | 
							
									  "id": "https://example.com/users/bob/interaction_requests/likes/12345",
							 | 
						||
| 
								 | 
							
									  "actor": "https://example.com/users/bob",
							 | 
						||
| 
								 | 
							
									  "object": "https://example.com/users/alice/statuses/1",
							 | 
						||
| 
								 | 
							
									  "to": "https://example.com/users/alice",
							 | 
						||
| 
								 | 
							
									  "instrument": {
							 | 
						||
| 
								 | 
							
									    "type": "Like",
							 | 
						||
| 
								 | 
							
									    "id": "https://example.com/users/bob/likes/12345",
							 | 
						||
| 
								 | 
							
									    "object": "https://example.com/users/alice/statuses/1",
							 | 
						||
| 
								 | 
							
									    "attributedTo": "https://example.com/users/bob",
							 | 
						||
| 
								 | 
							
									    "to": [
							 | 
						||
| 
								 | 
							
									      "https://www.w3.org/ns/activitystreams#Public",
							 | 
						||
| 
								 | 
							
									      "https://example.com/users/alice"
							 | 
						||
| 
								 | 
							
									    ]
							 | 
						||
| 
								 | 
							
									  }
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									Because each of the interaction types are a bit different,
							 | 
						||
| 
								 | 
							
									they're unfortunately also parsed and stored differently:
							 | 
						||
| 
								 | 
							
									LikeRequests have the Like checked first, here, against
							 | 
						||
| 
								 | 
							
									the interaction policy of the target status, whereas
							 | 
						||
| 
								 | 
							
									AnnounceRequests and ReplyRequests have the interaction
							 | 
						||
| 
								 | 
							
									checked against the interaction policy of the target
							 | 
						||
| 
								 | 
							
									status asynchronously, in the FromFediAPI processor.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									It may be possible to dick about with the logic a bit and
							 | 
						||
| 
								 | 
							
									shuffle the checks all here, or all in the processor, but
							 | 
						||
| 
								 | 
							
									that's a job for future refactoring by future tobi/kimbe.
							 | 
						||
| 
								 | 
							
								*/
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// partialInteractionRequest represents a
							 | 
						||
| 
								 | 
							
								// partially-parsed interaction request
							 | 
						||
| 
								 | 
							
								// returned from the util function parseInteractionReq.
							 | 
						||
| 
								 | 
							
								type partialInteractionRequest struct {
							 | 
						||
| 
								 | 
							
									intRequestURI string
							 | 
						||
| 
								 | 
							
									requesting    *gtsmodel.Account
							 | 
						||
| 
								 | 
							
									receiving     *gtsmodel.Account
							 | 
						||
| 
								 | 
							
									object        *gtsmodel.Status
							 | 
						||
| 
								 | 
							
									instrument    vocab.Type
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// parseIntReq does some first-pass parsing
							 | 
						||
| 
								 | 
							
								// of the given InteractionRequestable (LikeRequest,
							 | 
						||
| 
								 | 
							
								// ReplyRequest, AnnounceRequest), checking stuff like:
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								//   - interaction request has a single object
							 | 
						||
| 
								 | 
							
								//   - interaction request object is a status
							 | 
						||
| 
								 | 
							
								//   - object status belongs to receiving account
							 | 
						||
| 
								 | 
							
								//   - interaction request has a single instrument
							 | 
						||
| 
								 | 
							
								//
							 | 
						||
| 
								 | 
							
								// It returns a partialInteractionRequest struct,
							 | 
						||
| 
								 | 
							
								// or an error if something goes wrong.
							 | 
						||
| 
								 | 
							
								func (f *DB) parseInteractionRequest(ctx context.Context, intRequest ap.InteractionRequestable) (*partialInteractionRequest, error) {
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Get and stringify the ID/URI of interaction request once,
							 | 
						||
| 
								 | 
							
									// and mark this particular activity as handled in ID cache.
							 | 
						||
| 
								 | 
							
									intRequestURI := ap.GetJSONLDId(intRequest).String()
							 | 
						||
| 
								 | 
							
									f.activityIDs.Set(intRequestURI, struct{}{})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Extract relevant values from passed ctx.
							 | 
						||
| 
								 | 
							
									activityContext := getActivityContext(ctx)
							 | 
						||
| 
								 | 
							
									if activityContext.internal {
							 | 
						||
| 
								 | 
							
										// Already processed.
							 | 
						||
| 
								 | 
							
										return nil, nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									requesting := activityContext.requestingAcct
							 | 
						||
| 
								 | 
							
									receiving := activityContext.receivingAcct
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if requesting.IsMoving() {
							 | 
						||
| 
								 | 
							
										// A Moving account
							 | 
						||
| 
								 | 
							
										// can't do this.
							 | 
						||
| 
								 | 
							
										return nil, nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if receiving.IsMoving() {
							 | 
						||
| 
								 | 
							
										// Moving accounts can't
							 | 
						||
| 
								 | 
							
										// do anything with interaction
							 | 
						||
| 
								 | 
							
										// requests, so ignore it.
							 | 
						||
| 
								 | 
							
										return nil, nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Make sure we have a single
							 | 
						||
| 
								 | 
							
									// object of the interaction request.
							 | 
						||
| 
								 | 
							
									objectIRIs := ap.GetObjectIRIs(intRequest)
							 | 
						||
| 
								 | 
							
									if l := len(objectIRIs); l != 1 {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"invalid object len %d, wanted 1", l,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Extract the status URI str.
							 | 
						||
| 
								 | 
							
									statusIRI := objectIRIs[0]
							 | 
						||
| 
								 | 
							
									statusIRIStr := statusIRI.String()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Fetch status by given URI from the database.
							 | 
						||
| 
								 | 
							
									status, err := f.state.DB.GetStatusByURI(ctx, statusIRIStr)
							 | 
						||
| 
								 | 
							
									if err != nil && !errors.Is(err, db.ErrNoEntries) {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.Newf("db error getting object status %s: %w", statusIRIStr, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure received by correct account.
							 | 
						||
| 
								 | 
							
									if status.AccountID != receiving.ID {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusForbidden,
							 | 
						||
| 
								 | 
							
											"receiver %s is not owner of interaction-requested status",
							 | 
						||
| 
								 | 
							
											receiving.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure we have the expected one instrument.
							 | 
						||
| 
								 | 
							
									instruments := ap.ExtractInstruments(intRequest)
							 | 
						||
| 
								 | 
							
									if l := len(instruments); l != 1 {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"invalid instrument len %d, wanted 1", l,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Instrument should be a
							 | 
						||
| 
								 | 
							
									// type and not just an IRI.
							 | 
						||
| 
								 | 
							
									instrument := instruments[0].GetType()
							 | 
						||
| 
								 | 
							
									if instrument == nil {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"instrument was not vocab.Type",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Check the instrument is an approveable type.
							 | 
						||
| 
								 | 
							
									approvable, ok := instrument.(ap.WithApprovedBy)
							 | 
						||
| 
								 | 
							
									if !ok {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"instrument was not Approvable",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure that `approvedBy` isn't already set.
							 | 
						||
| 
								 | 
							
									if u := ap.GetApprovedBy(approvable); u != nil {
							 | 
						||
| 
								 | 
							
										return nil, gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"instrument claims to already be approvedBy %s",
							 | 
						||
| 
								 | 
							
											u.String(),
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return &partialInteractionRequest{
							 | 
						||
| 
								 | 
							
										intRequestURI: intRequestURI,
							 | 
						||
| 
								 | 
							
										requesting:    requesting,
							 | 
						||
| 
								 | 
							
										receiving:     receiving,
							 | 
						||
| 
								 | 
							
										object:        status,
							 | 
						||
| 
								 | 
							
										instrument:    instrument,
							 | 
						||
| 
								 | 
							
									}, nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (f *DB) LikeRequest(ctx context.Context, likeReq vocab.GoToSocialLikeRequest) error {
							 | 
						||
| 
								 | 
							
									log.DebugKV(ctx, "LikeRequest", serialize{likeReq})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Parse out base level interaction request information.
							 | 
						||
| 
								 | 
							
									partial, err := f.parseInteractionRequest(ctx, likeReq)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure the instrument vocab.Type is Likeable.
							 | 
						||
| 
								 | 
							
									likeable, ok := ap.ToLikeable(partial.instrument)
							 | 
						||
| 
								 | 
							
									if !ok {
							 | 
						||
| 
								 | 
							
										return gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"could not parse instrument to Likeable",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Convert received AS like type to internal fave model.
							 | 
						||
| 
								 | 
							
									fave, err := f.converter.ASLikeToFave(ctx, likeable)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										err := gtserror.Newf("error converting from AS type: %w", err)
							 | 
						||
| 
								 | 
							
										return gtserror.WrapWithCode(http.StatusBadRequest, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure fave enacted by correct account.
							 | 
						||
| 
								 | 
							
									if fave.AccountID != partial.requesting.ID {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusForbidden,
							 | 
						||
| 
								 | 
							
											"requester %s is not expected actor %s",
							 | 
						||
| 
								 | 
							
											partial.requesting.URI, fave.Account.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure fave received by correct account.
							 | 
						||
| 
								 | 
							
									if fave.TargetAccountID != partial.receiving.ID {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusForbidden,
							 | 
						||
| 
								 | 
							
											"receiver %s is not expected %s",
							 | 
						||
| 
								 | 
							
											partial.receiving.URI, fave.TargetAccount.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure this is a valid Like target for requester.
							 | 
						||
| 
								 | 
							
									policyResult, err := f.intFilter.StatusLikeable(ctx,
							 | 
						||
| 
								 | 
							
										partial.requesting,
							 | 
						||
| 
								 | 
							
										fave.Status,
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return gtserror.Newf(
							 | 
						||
| 
								 | 
							
											"error seeing if status %s is likeable: %w",
							 | 
						||
| 
								 | 
							
											fave.Status.URI, err,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									} else if policyResult.Forbidden() {
							 | 
						||
| 
								 | 
							
										return gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusForbidden,
							 | 
						||
| 
								 | 
							
											"requester does not have permission to Like status",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Policy result is either automatic or manual
							 | 
						||
| 
								 | 
							
									// approval, so store the interaction request.
							 | 
						||
| 
								 | 
							
									intReq := >smodel.InteractionRequest{
							 | 
						||
| 
								 | 
							
										ID:                    id.NewULID(),
							 | 
						||
| 
								 | 
							
										TargetStatusID:        fave.StatusID,
							 | 
						||
| 
								 | 
							
										TargetStatus:          fave.Status,
							 | 
						||
| 
								 | 
							
										TargetAccountID:       fave.TargetAccountID,
							 | 
						||
| 
								 | 
							
										TargetAccount:         fave.TargetAccount,
							 | 
						||
| 
								 | 
							
										InteractingAccountID:  fave.AccountID,
							 | 
						||
| 
								 | 
							
										InteractingAccount:    fave.Account,
							 | 
						||
| 
								 | 
							
										InteractionRequestURI: partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										InteractionURI:        fave.URI,
							 | 
						||
| 
								 | 
							
										InteractionType:       gtsmodel.InteractionLike,
							 | 
						||
| 
								 | 
							
										Polite:                util.Ptr(true),
							 | 
						||
| 
								 | 
							
										Like:                  fave,
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									switch err := f.state.DB.PutInteractionRequest(ctx, intReq); {
							 | 
						||
| 
								 | 
							
									case err == nil:
							 | 
						||
| 
								 | 
							
										// No problem.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									case errors.Is(err, db.ErrAlreadyExists):
							 | 
						||
| 
								 | 
							
										// Already processed this, race condition? Just warn + return.
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received duplicate interaction request: %s", partial.intRequestURI)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									default:
							 | 
						||
| 
								 | 
							
										// Proper DB error.
							 | 
						||
| 
								 | 
							
										return gtserror.Newf(
							 | 
						||
| 
								 | 
							
											"db error storing interaction request %s",
							 | 
						||
| 
								 | 
							
											partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Int req is now stored.
							 | 
						||
| 
								 | 
							
									//
							 | 
						||
| 
								 | 
							
									// Set some fields on the
							 | 
						||
| 
								 | 
							
									// pending fave and store it.
							 | 
						||
| 
								 | 
							
									fave.ID = id.NewULID()
							 | 
						||
| 
								 | 
							
									fave.PendingApproval = util.Ptr(true)
							 | 
						||
| 
								 | 
							
									fave.PreApproved = policyResult.AutomaticApproval()
							 | 
						||
| 
								 | 
							
									if err := f.state.DB.PutStatusFave(ctx, fave); err != nil {
							 | 
						||
| 
								 | 
							
										if errors.Is(err, db.ErrAlreadyExists) {
							 | 
						||
| 
								 | 
							
											// The fave already exists in the
							 | 
						||
| 
								 | 
							
											// database, which means we've already
							 | 
						||
| 
								 | 
							
											// handled side effects. We can just
							 | 
						||
| 
								 | 
							
											// return nil here and be done with it.
							 | 
						||
| 
								 | 
							
											return nil
							 | 
						||
| 
								 | 
							
										}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
										return gtserror.Newf("error inserting %s into db: %w", fave.URI, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Further processing will be carried out
							 | 
						||
| 
								 | 
							
									// asynchronously, and our caller will return 202 Accepted.
							 | 
						||
| 
								 | 
							
									f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
							 | 
						||
| 
								 | 
							
										APActivityType: ap.ActivityCreate,
							 | 
						||
| 
								 | 
							
										APObjectType:   ap.ActivityLikeRequest,
							 | 
						||
| 
								 | 
							
										GTSModel:       intReq,
							 | 
						||
| 
								 | 
							
										Receiving:      partial.receiving,
							 | 
						||
| 
								 | 
							
										Requesting:     partial.requesting,
							 | 
						||
| 
								 | 
							
									})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (f *DB) ReplyRequest(ctx context.Context, replyReq vocab.GoToSocialReplyRequest) error {
							 | 
						||
| 
								 | 
							
									log.DebugKV(ctx, "ReplyRequest", serialize{replyReq})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Parse out base level interaction request information.
							 | 
						||
| 
								 | 
							
									partial, err := f.parseInteractionRequest(ctx, replyReq)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure the instrument vocab.Type is Statusable.
							 | 
						||
| 
								 | 
							
									statusable, ok := ap.ToStatusable(partial.instrument)
							 | 
						||
| 
								 | 
							
									if !ok {
							 | 
						||
| 
								 | 
							
										return gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"could not parse instrument to Statusable",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Check for spam / relevance.
							 | 
						||
| 
								 | 
							
									ok, err = f.statusableOK(
							 | 
						||
| 
								 | 
							
										ctx,
							 | 
						||
| 
								 | 
							
										partial.receiving,
							 | 
						||
| 
								 | 
							
										partial.requesting,
							 | 
						||
| 
								 | 
							
										statusable,
							 | 
						||
| 
								 | 
							
									)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										// Error already
							 | 
						||
| 
								 | 
							
										// wrapped.
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if !ok {
							 | 
						||
| 
								 | 
							
										// Not relevant / spam.
							 | 
						||
| 
								 | 
							
										// Already logged.
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Statusable must reply to something.
							 | 
						||
| 
								 | 
							
									inReplyToURIs := ap.GetInReplyTo(statusable)
							 | 
						||
| 
								 | 
							
									if l := len(inReplyToURIs); l != 1 {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"expected inReplyTo length 1, got %d", l,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									inReplyToURI := inReplyToURIs[0]
							 | 
						||
| 
								 | 
							
									inReplyToURIStr := inReplyToURI.String()
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Make sure we have the status this interaction reply encompasses.
							 | 
						||
| 
								 | 
							
									inReplyTo, err := f.state.DB.GetStatusByURI(ctx, inReplyToURIStr)
							 | 
						||
| 
								 | 
							
									if err != nil && !errors.Is(err, db.ErrNoEntries) {
							 | 
						||
| 
								 | 
							
										return gtserror.Newf("db error getting inReplyTo status %s: %w", inReplyToURIStr, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Check status exists.
							 | 
						||
| 
								 | 
							
									if inReplyTo == nil {
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received ReplyRequest for non-existent status: %s", inReplyToURIStr)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Make sure the parent status is owned by receiver.
							 | 
						||
| 
								 | 
							
									if inReplyTo.AccountURI != partial.receiving.URI {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"inReplyTo status %s not owned by receiving account %s",
							 | 
						||
| 
								 | 
							
											inReplyToURIStr, partial.receiving.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Extract the attributed to (i.e. author) URI of status.
							 | 
						||
| 
								 | 
							
									attributedToURI, err := ap.ExtractAttributedToURI(statusable)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										err := gtserror.Newf("invalid status attributedTo value: %w", err)
							 | 
						||
| 
								 | 
							
										return gtserror.WrapWithCode(http.StatusBadRequest, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure status author is account of requester.
							 | 
						||
| 
								 | 
							
									attributedToURIStr := attributedToURI.String()
							 | 
						||
| 
								 | 
							
									if attributedToURIStr != partial.requesting.URI {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"status attributedTo %s not requesting account %s",
							 | 
						||
| 
								 | 
							
											inReplyToURIStr, partial.requesting.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Create a pending interaction request in the database.
							 | 
						||
| 
								 | 
							
									// This request will be handled further by the processor.
							 | 
						||
| 
								 | 
							
									intReq := >smodel.InteractionRequest{
							 | 
						||
| 
								 | 
							
										ID:                    id.NewULID(),
							 | 
						||
| 
								 | 
							
										TargetStatusID:        inReplyTo.ID,
							 | 
						||
| 
								 | 
							
										TargetStatus:          inReplyTo,
							 | 
						||
| 
								 | 
							
										TargetAccountID:       inReplyTo.AccountID,
							 | 
						||
| 
								 | 
							
										TargetAccount:         inReplyTo.Account,
							 | 
						||
| 
								 | 
							
										InteractingAccountID:  partial.requesting.ID,
							 | 
						||
| 
								 | 
							
										InteractingAccount:    partial.requesting,
							 | 
						||
| 
								 | 
							
										InteractionRequestURI: partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										InteractionURI:        ap.GetJSONLDId(statusable).String(),
							 | 
						||
| 
								 | 
							
										InteractionType:       gtsmodel.InteractionReply,
							 | 
						||
| 
								 | 
							
										Polite:                util.Ptr(true),
							 | 
						||
| 
								 | 
							
										Reply:                 nil, // Not settable yet.
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									switch err := f.state.DB.PutInteractionRequest(ctx, intReq); {
							 | 
						||
| 
								 | 
							
									case err == nil:
							 | 
						||
| 
								 | 
							
										// No problem.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									case errors.Is(err, db.ErrAlreadyExists):
							 | 
						||
| 
								 | 
							
										// Already processed this, race condition? Just warn + return.
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received duplicate interaction request: %s", partial.intRequestURI)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									default:
							 | 
						||
| 
								 | 
							
										// Proper DB error.
							 | 
						||
| 
								 | 
							
										return gtserror.Newf(
							 | 
						||
| 
								 | 
							
											"db error storing interaction request %s",
							 | 
						||
| 
								 | 
							
											partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Further processing will be carried out
							 | 
						||
| 
								 | 
							
									// asynchronously, return 202 Accepted.
							 | 
						||
| 
								 | 
							
									f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
							 | 
						||
| 
								 | 
							
										APActivityType: ap.ActivityCreate,
							 | 
						||
| 
								 | 
							
										APObjectType:   ap.ActivityReplyRequest,
							 | 
						||
| 
								 | 
							
										GTSModel:       intReq,
							 | 
						||
| 
								 | 
							
										APObject:       statusable,
							 | 
						||
| 
								 | 
							
										Receiving:      partial.receiving,
							 | 
						||
| 
								 | 
							
										Requesting:     partial.requesting,
							 | 
						||
| 
								 | 
							
									})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return nil
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								func (f *DB) AnnounceRequest(ctx context.Context, announceReq vocab.GoToSocialAnnounceRequest) error {
							 | 
						||
| 
								 | 
							
									log.DebugKV(ctx, "AnnounceRequest", serialize{announceReq})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Parse out base level interaction request information.
							 | 
						||
| 
								 | 
							
									partial, err := f.parseInteractionRequest(ctx, announceReq)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										return err
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure the instrument vocab.Type is Announceable.
							 | 
						||
| 
								 | 
							
									announceable, ok := ap.ToAnnounceable(partial.instrument)
							 | 
						||
| 
								 | 
							
									if !ok {
							 | 
						||
| 
								 | 
							
										return gtserror.NewWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"could not parse instrument to Announceable",
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Convert received AS Announce type to internal boost wrapper model.
							 | 
						||
| 
								 | 
							
									boost, new, err := f.converter.ASAnnounceToStatus(ctx, announceable)
							 | 
						||
| 
								 | 
							
									if err != nil {
							 | 
						||
| 
								 | 
							
										err := gtserror.Newf("error converting from AS type: %w", err)
							 | 
						||
| 
								 | 
							
										return gtserror.WrapWithCode(http.StatusBadRequest, err)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if !new {
							 | 
						||
| 
								 | 
							
										// We already have this announce, just return.
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received AnnounceRequest for existing announce: %s", boost.URI)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Fetch origin status that this boost is targetting from database.
							 | 
						||
| 
								 | 
							
									targetStatus, err := f.state.DB.GetStatusByURI(ctx, boost.BoostOfURI)
							 | 
						||
| 
								 | 
							
									if err != nil && !errors.Is(err, db.ErrNoEntries) {
							 | 
						||
| 
								 | 
							
										return gtserror.Newf(
							 | 
						||
| 
								 | 
							
											"db error getting announce object %s: %w",
							 | 
						||
| 
								 | 
							
											boost.BoostOfURI, err,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									if targetStatus == nil {
							 | 
						||
| 
								 | 
							
										// Status doesn't seem to exist, just drop this AnnounceRequest.
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received AnnounceRequest for non-existent status %s", boost.BoostOfURI)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure target status is owned by receiving account.
							 | 
						||
| 
								 | 
							
									if targetStatus.AccountID != partial.receiving.ID {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusBadRequest,
							 | 
						||
| 
								 | 
							
											"announce object %s not owned by receiving account %s",
							 | 
						||
| 
								 | 
							
											boost.BoostOfURI, partial.receiving.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Ensure announce enacted by correct account.
							 | 
						||
| 
								 | 
							
									if boost.AccountID != partial.requesting.ID {
							 | 
						||
| 
								 | 
							
										return gtserror.NewfWithCode(
							 | 
						||
| 
								 | 
							
											http.StatusForbidden,
							 | 
						||
| 
								 | 
							
											"requester %s is not expected actor %s",
							 | 
						||
| 
								 | 
							
											partial.requesting.URI, boost.Account.URI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Create a pending interaction request in the database.
							 | 
						||
| 
								 | 
							
									// This request will be handled further by the processor.
							 | 
						||
| 
								 | 
							
									intReq := >smodel.InteractionRequest{
							 | 
						||
| 
								 | 
							
										ID:                    id.NewULID(),
							 | 
						||
| 
								 | 
							
										TargetStatusID:        targetStatus.ID,
							 | 
						||
| 
								 | 
							
										TargetStatus:          targetStatus,
							 | 
						||
| 
								 | 
							
										TargetAccountID:       targetStatus.AccountID,
							 | 
						||
| 
								 | 
							
										TargetAccount:         targetStatus.Account,
							 | 
						||
| 
								 | 
							
										InteractingAccountID:  boost.AccountID,
							 | 
						||
| 
								 | 
							
										InteractingAccount:    boost.Account,
							 | 
						||
| 
								 | 
							
										InteractionRequestURI: partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										InteractionURI:        boost.URI,
							 | 
						||
| 
								 | 
							
										InteractionType:       gtsmodel.InteractionAnnounce,
							 | 
						||
| 
								 | 
							
										Polite:                util.Ptr(true),
							 | 
						||
| 
								 | 
							
										Announce:              boost,
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
									switch err := f.state.DB.PutInteractionRequest(ctx, intReq); {
							 | 
						||
| 
								 | 
							
									case err == nil:
							 | 
						||
| 
								 | 
							
										// No problem.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									case errors.Is(err, db.ErrAlreadyExists):
							 | 
						||
| 
								 | 
							
										// Already processed this, race condition? Just warn + return.
							 | 
						||
| 
								 | 
							
										log.Warnf(ctx, "received duplicate interaction request: %s", partial.intRequestURI)
							 | 
						||
| 
								 | 
							
										return nil
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									default:
							 | 
						||
| 
								 | 
							
										// Proper DB error.
							 | 
						||
| 
								 | 
							
										return gtserror.Newf(
							 | 
						||
| 
								 | 
							
											"db error storing interaction request %s",
							 | 
						||
| 
								 | 
							
											partial.intRequestURI,
							 | 
						||
| 
								 | 
							
										)
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									// Further processing will be carried out
							 | 
						||
| 
								 | 
							
									// asynchronously, return 202 Accepted.
							 | 
						||
| 
								 | 
							
									f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
							 | 
						||
| 
								 | 
							
										APActivityType: ap.ActivityCreate,
							 | 
						||
| 
								 | 
							
										APObjectType:   ap.ActivityAnnounceRequest,
							 | 
						||
| 
								 | 
							
										GTSModel:       intReq,
							 | 
						||
| 
								 | 
							
										Receiving:      partial.receiving,
							 | 
						||
| 
								 | 
							
										Requesting:     partial.requesting,
							 | 
						||
| 
								 | 
							
									})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
									return nil
							 | 
						||
| 
								 | 
							
								}
							 |