mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 01:52:26 -05:00
[feature] Support new model of interaction flow for forward compat with v0.21.0 (#4394)
~~Still WIP!~~ This PR allows v0.20.0 of GtS to be forward-compatible with the interaction request / authorization flow that will fully replace the current flow in v0.21.0. Basically, this means we need to recognize LikeRequest, ReplyRequest, and AnnounceRequest, and in response to those requests, deliver either a Reject or an Accept, with the latter pointing towards a LikeAuthorization, ReplyAuthorization, or AnnounceAuthorization, respectively. This can then be used by the remote instance to prove to third parties that the interaction has been accepted by the interactee. These Authorization types need to be dereferencable to third parties, so we need to serve them. As well as recognizing the above "polite" interaction request types, we also need to still serve appropriate responses to "impolite" interaction request types, where an instance that's unaware of interaction policies tries to interact with a post by sending a reply, like, or boost directly, without wrapping it in a WhateverRequest type. Doesn't fully close https://codeberg.org/superseriousbusiness/gotosocial/issues/4026 but gets damn near (just gotta update the federating with GtS documentation). Migrations tested on both Postgres and SQLite. Co-authored-by: kim <grufwub@gmail.com> Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4394 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
parent
33fed81a8d
commit
754b7be9cf
126 changed files with 6637 additions and 1778 deletions
|
|
@ -70,7 +70,7 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
switch {
|
||||
case status.Account.IsSuspended():
|
||||
// we shouldn't reach this point, log to poke devs to investigate.
|
||||
log.Warnf(ctx, "status author suspended: %s", status.AccountURI)
|
||||
log.Warnf(ctx, "should not have reached here, author suspended: %s", status.AccountURI)
|
||||
permitted = false
|
||||
|
||||
case status.InReplyToURI != "":
|
||||
|
|
@ -111,7 +111,8 @@ func (d *Dereferencer) isPermittedStatus(
|
|||
return
|
||||
}
|
||||
|
||||
// isPermittedReply ...
|
||||
// isPermittedReply checks whether the given status
|
||||
// is a permitted reply to its referenced inReplyTo.
|
||||
func (d *Dereferencer) isPermittedReply(
|
||||
ctx context.Context,
|
||||
requestUser string,
|
||||
|
|
@ -119,20 +120,21 @@ func (d *Dereferencer) isPermittedReply(
|
|||
) (bool, error) {
|
||||
|
||||
var (
|
||||
replyURI = reply.URI // Definitely set.
|
||||
inReplyToURI = reply.InReplyToURI // Definitely set.
|
||||
inReplyTo = reply.InReplyTo // Might not be set.
|
||||
replyURI = reply.URI // Definitely set.
|
||||
|
||||
parentURI = reply.InReplyToURI // Definitely set.
|
||||
parent = reply.InReplyTo // Might not be set.
|
||||
|
||||
approvedByURI = reply.ApprovedByURI // Might not be set.
|
||||
)
|
||||
|
||||
// Check if we have a stored interaction request for parent status.
|
||||
parentReq, err := d.state.DB.GetInteractionRequestByInteractionURI(
|
||||
gtscontext.SetBarebones(ctx),
|
||||
inReplyToURI,
|
||||
parentURI,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting interaction request: %w", err)
|
||||
return false, err
|
||||
return false, gtserror.Newf("db error getting interaction request: %w", err)
|
||||
}
|
||||
|
||||
// Check if we have a stored interaction request for this reply.
|
||||
|
|
@ -141,8 +143,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
replyURI,
|
||||
)
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
err := gtserror.Newf("db error getting interaction request: %w", err)
|
||||
return false, err
|
||||
return false, gtserror.Newf("db error getting interaction request: %w", err)
|
||||
}
|
||||
|
||||
parentRejected := (parentReq != nil && parentReq.IsRejected())
|
||||
|
|
@ -176,8 +177,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// it was rejected previously and now claims
|
||||
// to be approved. Continue permission checks.
|
||||
|
||||
if inReplyTo == nil {
|
||||
|
||||
if parent == nil {
|
||||
// If we didn't have the replied-to status
|
||||
// in our database (yet), we can't check
|
||||
// right now if this reply is permitted.
|
||||
|
|
@ -191,24 +191,23 @@ func (d *Dereferencer) isPermittedReply(
|
|||
}
|
||||
|
||||
// We have the replied-to status; ensure it's fully populated.
|
||||
if err := d.state.DB.PopulateStatus(ctx, inReplyTo); err != nil {
|
||||
if err := d.state.DB.PopulateStatus(ctx, parent); err != nil {
|
||||
return false, gtserror.Newf("error populating status %s: %w", reply.ID, err)
|
||||
}
|
||||
|
||||
// Make sure replied-to status is not
|
||||
// a boost wrapper, and make sure it's
|
||||
// actually visible to the requester.
|
||||
if inReplyTo.BoostOfID != "" {
|
||||
// We do not permit replies
|
||||
// to boost wrapper statuses.
|
||||
log.Info(ctx, "rejecting reply to boost wrapper status")
|
||||
// Boost wrapper statuses
|
||||
// cannot receive replies.
|
||||
if parent.BoostOfID != "" {
|
||||
log.Warn(ctx, "received reply to boost wrapper status: %s", parent.URI)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if inReplyTo.IsLocal() {
|
||||
// If parent is a local status
|
||||
// check visibility to replyer.
|
||||
if parent.IsLocal() {
|
||||
visible, err := d.visFilter.StatusVisible(ctx,
|
||||
reply.Account,
|
||||
inReplyTo,
|
||||
parent,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking inReplyTo visibility: %w", err)
|
||||
|
|
@ -227,12 +226,12 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// approval and checking the return value.
|
||||
// No further checks are required.
|
||||
if approvedByURI != "" {
|
||||
return d.isPermittedByApprovedByIRI(
|
||||
return d.isPermittedByAuthURI(
|
||||
ctx,
|
||||
gtsmodel.InteractionReply,
|
||||
requestUser,
|
||||
reply,
|
||||
inReplyTo,
|
||||
parent,
|
||||
thisReq,
|
||||
approvedByURI,
|
||||
)
|
||||
|
|
@ -243,7 +242,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// to see what we need to do with it.
|
||||
replyable, err := d.intFilter.StatusReplyable(ctx,
|
||||
reply.Account,
|
||||
inReplyTo,
|
||||
parent,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("error checking status replyability: %w", err)
|
||||
|
|
@ -260,7 +259,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
return false, d.rejectedByPolicy(
|
||||
ctx,
|
||||
reply,
|
||||
inReplyTo,
|
||||
parent,
|
||||
thisReq,
|
||||
)
|
||||
}
|
||||
|
|
@ -279,7 +278,7 @@ func (d *Dereferencer) isPermittedReply(
|
|||
// pending approval, though we know at this point
|
||||
// that the status did not include an approvedBy URI.
|
||||
|
||||
if !inReplyTo.IsLocal() {
|
||||
if !parent.IsLocal() {
|
||||
// If the replied-to status is remote, we should just
|
||||
// drop this reply at this point, as we can't verify
|
||||
// that the remote replied-to account approves it, and
|
||||
|
|
@ -359,7 +358,7 @@ func (d *Dereferencer) unpermittedByParent(
|
|||
// This collapses the chain beyond the first
|
||||
// rejected reply and allows us to avoid derefing
|
||||
// further replies we already know we don't want.
|
||||
inReplyToID := parentReq.StatusID
|
||||
inReplyToID := parentReq.TargetStatusID
|
||||
targetAccountID := parentReq.TargetAccountID
|
||||
|
||||
// As nobody is actually Rejecting the reply
|
||||
|
|
@ -369,14 +368,16 @@ func (d *Dereferencer) unpermittedByParent(
|
|||
uri := ""
|
||||
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: rejectID,
|
||||
StatusID: inReplyToID,
|
||||
TargetAccountID: targetAccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: uri,
|
||||
RejectedAt: time.Now(),
|
||||
ID: rejectID,
|
||||
TargetStatusID: inReplyToID,
|
||||
TargetAccountID: targetAccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(reply.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
ResponseURI: uri,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
|
@ -386,14 +387,14 @@ func (d *Dereferencer) unpermittedByParent(
|
|||
return nil
|
||||
}
|
||||
|
||||
// isPermittedByApprovedByIRI checks whether the given URI
|
||||
// isPermittedByAuthURI checks whether the given URI
|
||||
// can be dereferenced, and whether it returns either an
|
||||
// Accept activity or an approval object which permits the
|
||||
// given reply to the given inReplyTo status.
|
||||
// Accept activity or an authorization object that permits
|
||||
// the given reply to the given inReplyTo status.
|
||||
//
|
||||
// If yes, then thisReq will be updated to
|
||||
// reflect the approval, if it's not nil.
|
||||
func (d *Dereferencer) isPermittedByApprovedByIRI(
|
||||
func (d *Dereferencer) isPermittedByAuthURI(
|
||||
ctx context.Context,
|
||||
interactionType gtsmodel.InteractionType,
|
||||
requestUser string,
|
||||
|
|
@ -402,7 +403,7 @@ func (d *Dereferencer) isPermittedByApprovedByIRI(
|
|||
thisReq *gtsmodel.InteractionRequest,
|
||||
approvedByIRI string,
|
||||
) (bool, error) {
|
||||
permitted, err := d.isValidApprovedByIRI(
|
||||
permitted, err := d.isValidAuthURI(
|
||||
ctx,
|
||||
interactionType,
|
||||
requestUser,
|
||||
|
|
@ -430,13 +431,13 @@ func (d *Dereferencer) isPermittedByApprovedByIRI(
|
|||
// pending approval, clear that now.
|
||||
reply.PendingApproval = util.Ptr(false)
|
||||
if thisReq != nil {
|
||||
thisReq.URI = approvedByIRI
|
||||
thisReq.ResponseURI = approvedByIRI
|
||||
thisReq.AcceptedAt = time.Now()
|
||||
thisReq.RejectedAt = time.Time{}
|
||||
err := d.state.DB.UpdateInteractionRequest(
|
||||
ctx,
|
||||
thisReq,
|
||||
"uri",
|
||||
"response_uri",
|
||||
"accepted_at",
|
||||
"rejected_at",
|
||||
)
|
||||
|
|
@ -483,13 +484,13 @@ func (d *Dereferencer) rejectedByPolicy(
|
|||
// request is marked as rejected.
|
||||
thisReq.RejectedAt = time.Now()
|
||||
thisReq.AcceptedAt = time.Time{}
|
||||
thisReq.URI = rejectURI
|
||||
thisReq.ResponseURI = rejectURI
|
||||
err := d.state.DB.UpdateInteractionRequest(
|
||||
ctx,
|
||||
thisReq,
|
||||
"rejected_at",
|
||||
"accepted_at",
|
||||
"uri",
|
||||
"response_uri",
|
||||
)
|
||||
if err != nil {
|
||||
return gtserror.Newf("db error updating interaction request: %w", err)
|
||||
|
|
@ -501,14 +502,16 @@ func (d *Dereferencer) rejectedByPolicy(
|
|||
// We haven't stored a rejected interaction
|
||||
// request for this status yet, do it now.
|
||||
rejection := >smodel.InteractionRequest{
|
||||
ID: rejectID,
|
||||
StatusID: inReplyTo.ID,
|
||||
TargetAccountID: inReplyTo.AccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
URI: rejectURI,
|
||||
RejectedAt: time.Now(),
|
||||
ID: rejectID,
|
||||
TargetStatusID: inReplyTo.ID,
|
||||
TargetAccountID: inReplyTo.AccountID,
|
||||
InteractingAccountID: reply.AccountID,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(reply.URI, gtsmodel.ReplyRequestSuffix),
|
||||
InteractionURI: reply.URI,
|
||||
InteractionType: gtsmodel.InteractionReply,
|
||||
Polite: util.Ptr(false),
|
||||
ResponseURI: rejectURI,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
err := d.state.DB.PutInteractionRequest(ctx, rejection)
|
||||
if err != nil && !errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
|
@ -611,7 +614,7 @@ func (d *Dereferencer) isPermittedBoost(
|
|||
// Boost claims to be approved, check
|
||||
// this by dereferencing the approvedBy
|
||||
// and inspecting the return value.
|
||||
permitted, err := d.isValidApprovedByIRI(
|
||||
permitted, err := d.isValidAuthURI(
|
||||
ctx,
|
||||
gtsmodel.InteractionAnnounce,
|
||||
requestUser,
|
||||
|
|
@ -637,36 +640,36 @@ func (d *Dereferencer) isPermittedBoost(
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// isValidApprovedByIRI dereferences the activitystreams Accept or approval
|
||||
// at the specified IRI, and checks the Accept or approval for validity
|
||||
// against the provided expectedActor, expectedObject, and expectedTarget.
|
||||
// isValidAuthURI dereferences the activitystreams Accept or authorization
|
||||
// at the specified IRI, and checks it for validity against the provided
|
||||
// expectedActor, expectedObject, and expectedTarget.
|
||||
//
|
||||
// Will return either (true, nil) if everything looked OK, an error
|
||||
// if something went wrong internally during deref, or (false, nil)
|
||||
// if the dereferenced Accept/Approval did not meet expectations.
|
||||
func (d *Dereferencer) isValidApprovedByIRI(
|
||||
func (d *Dereferencer) isValidAuthURI(
|
||||
ctx context.Context,
|
||||
interactionType gtsmodel.InteractionType,
|
||||
requestUser string,
|
||||
approvedByIRIStr string, // approval uri Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03"
|
||||
authIRIStr string, // authorization uri Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03"
|
||||
expectActorURIStr string, // actor Eg., "https://example.org/users/someone"
|
||||
expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
|
||||
expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E"
|
||||
) (bool, error) {
|
||||
l := log.
|
||||
WithContext(ctx).
|
||||
WithField("approvedByIRI", approvedByIRIStr)
|
||||
WithField("authIRI", authIRIStr)
|
||||
|
||||
approvedByIRI, err := url.Parse(approvedByIRIStr)
|
||||
authIRI, err := url.Parse(authIRIStr)
|
||||
if err != nil {
|
||||
// Real returnable error.
|
||||
err := gtserror.Newf("error parsing approvedByIRI: %w", err)
|
||||
err := gtserror.Newf("error parsing authIRI: %w", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Don't make calls to the IRI if its
|
||||
// domain is blocked, just return false.
|
||||
blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByIRI.Host)
|
||||
blocked, err := d.state.DB.IsDomainBlocked(ctx, authIRI.Host)
|
||||
if err != nil {
|
||||
// Real returnable error.
|
||||
err := gtserror.Newf("error checking domain block: %w", err)
|
||||
|
|
@ -674,7 +677,7 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
}
|
||||
|
||||
if blocked {
|
||||
l.Info("approvedByIRI host is blocked")
|
||||
l.Info("authIRI host is blocked")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -685,12 +688,12 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
return false, err
|
||||
}
|
||||
|
||||
// Make the call to the approvedByURI.
|
||||
// Make the call to the authIRI.
|
||||
// Log any error encountered here but don't
|
||||
// return it as it's not *our* error.
|
||||
rsp, err := tsport.Dereference(ctx, approvedByIRI)
|
||||
rsp, err := tsport.Dereference(ctx, authIRI)
|
||||
if err != nil {
|
||||
l.Errorf("error dereferencing approvedByIRI: %v", err)
|
||||
l.Errorf("error dereferencing authIRI: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -706,31 +709,31 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
}
|
||||
|
||||
// Extract the URI/ID of the type.
|
||||
approvedByID := ap.GetJSONLDId(t)
|
||||
approvedByIDStr := approvedByID.String()
|
||||
authID := ap.GetJSONLDId(t)
|
||||
authIDStr := authID.String()
|
||||
|
||||
// Check whether input URI and final returned URI
|
||||
// have changed (i.e. we followed some redirects).
|
||||
rspURL := rsp.Request.URL
|
||||
rspURLStr := rspURL.String()
|
||||
if rspURLStr != approvedByIRIStr {
|
||||
if rspURLStr != authIRIStr {
|
||||
// If rspURLStr != approvedByIRI, make sure final
|
||||
// response URL is at least on the same host as
|
||||
// what we expected (ie., we weren't redirected
|
||||
// across domains), and make sure it's the same
|
||||
// as the ID of the Accept we were returned.
|
||||
switch {
|
||||
case rspURL.Host != approvedByIRI.Host:
|
||||
case rspURL.Host != authIRI.Host:
|
||||
l.Errorf(
|
||||
"final deref host %s did not match approvedByIRI host",
|
||||
"final deref host %s did not match authIRI host",
|
||||
rspURL.Host,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
case approvedByIDStr != rspURLStr:
|
||||
case authIDStr != rspURLStr:
|
||||
l.Errorf(
|
||||
"final deref uri %s did not match returned ID %s",
|
||||
rspURLStr, approvedByIDStr,
|
||||
rspURLStr, authIDStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -739,13 +742,13 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
// Response is superficially OK,
|
||||
// check in more detail now.
|
||||
|
||||
// First try to parse type as Approval stamp.
|
||||
if approvable, ok := ap.ToApprovable(t); ok {
|
||||
return isValidApprovable(
|
||||
// First try to parse type as Authorization stamp.
|
||||
if authable, ok := ap.ToAuthorizationable(t); ok {
|
||||
return isValidAuthorization(
|
||||
ctx,
|
||||
interactionType,
|
||||
approvable,
|
||||
approvedByID,
|
||||
authable,
|
||||
authID,
|
||||
expectActorURIStr, // actor
|
||||
expectObjectURIStr, // object
|
||||
expectTargetURIStr, // target
|
||||
|
|
@ -757,7 +760,7 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
return isValidAcceptable(
|
||||
ctx,
|
||||
acceptable,
|
||||
approvedByID,
|
||||
authID,
|
||||
expectActorURIStr, // actor
|
||||
expectObjectURIStr, // object
|
||||
expectTargetURIStr, // target
|
||||
|
|
@ -767,8 +770,8 @@ func (d *Dereferencer) isValidApprovedByIRI(
|
|||
// Type wasn't something we
|
||||
// could do anything with!
|
||||
l.Errorf(
|
||||
"%T at %s not approvable or acceptable",
|
||||
t, approvedByIRIStr,
|
||||
"%T at %s not authorization or accept",
|
||||
t, authIRIStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
|
@ -850,55 +853,55 @@ func isValidAcceptable(
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func isValidApprovable(
|
||||
func isValidAuthorization(
|
||||
ctx context.Context,
|
||||
interactionType gtsmodel.InteractionType,
|
||||
approvable ap.Approvable,
|
||||
approvalID *url.URL,
|
||||
auth ap.Authorizationable,
|
||||
authID *url.URL,
|
||||
expectActorURIStr string, // actor Eg., "https://example.org/users/someone"
|
||||
expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R"
|
||||
expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E"
|
||||
) (bool, error) {
|
||||
l := log.
|
||||
WithContext(ctx).
|
||||
WithField("approval", approvalID.String())
|
||||
WithField("auth", authID.String())
|
||||
|
||||
// Check that the type of the Approval
|
||||
// Check that the type of the Authorization
|
||||
// matches the interaction it's approving.
|
||||
switch tn := approvable.GetTypeName(); {
|
||||
case (tn == ap.ObjectLikeApproval && interactionType == gtsmodel.InteractionLike),
|
||||
(tn == ap.ObjectReplyApproval && interactionType == gtsmodel.InteractionReply),
|
||||
(tn == ap.ObjectAnnounceApproval && interactionType == gtsmodel.InteractionAnnounce):
|
||||
switch tn := auth.GetTypeName(); {
|
||||
case (tn == ap.ObjectLikeAuthorization && interactionType == gtsmodel.InteractionLike),
|
||||
(tn == ap.ObjectReplyAuthorization && interactionType == gtsmodel.InteractionReply),
|
||||
(tn == ap.ObjectAnnounceAuthorization && interactionType == gtsmodel.InteractionAnnounce):
|
||||
// All good baby!
|
||||
default:
|
||||
// There's a mismatch.
|
||||
l.Errorf(
|
||||
"approval type %s cannot approve %s",
|
||||
"authorization type %s cannot approve %s",
|
||||
tn, interactionType.String(),
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Extract the actor IRI and string from Approval.
|
||||
actorIRIs := ap.GetAttributedTo(approvable)
|
||||
actorIRIs := ap.GetAttributedTo(auth)
|
||||
actorIRI, actorIRIStr := extractIRI(actorIRIs)
|
||||
switch {
|
||||
case actorIRIStr == "":
|
||||
l.Error("Approval missing attributedTo IRI")
|
||||
l.Error("authorization missing attributedTo IRI")
|
||||
return false, nil
|
||||
|
||||
// Ensure the Approval actor is on
|
||||
// Ensure the authorization actor is on
|
||||
// the instance hosting the Approval.
|
||||
case actorIRI.Host != approvalID.Host:
|
||||
case actorIRI.Host != authID.Host:
|
||||
l.Errorf(
|
||||
"actor %s not on the same host as Approval",
|
||||
"actor %s not on the same host as authorization",
|
||||
actorIRIStr,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
// Ensure the Approval actor is who we expect
|
||||
// Ensure the auth actor is who we expect
|
||||
// it to be, and not someone else trying to
|
||||
// do an Approval for an interaction with a
|
||||
// do an auth for an interaction with a
|
||||
// statusable they don't own.
|
||||
case actorIRIStr != expectActorURIStr:
|
||||
l.Errorf(
|
||||
|
|
@ -908,33 +911,32 @@ func isValidApprovable(
|
|||
return false, nil
|
||||
}
|
||||
|
||||
// Extract the object IRI string from Approval.
|
||||
objectIRIs := ap.GetObjectIRIs(approvable)
|
||||
// Extract the object IRI string from authorization.
|
||||
objectIRIs := ap.GetInteractingObject(auth)
|
||||
_, objectIRIStr := extractIRI(objectIRIs)
|
||||
switch {
|
||||
case objectIRIStr == "":
|
||||
l.Error("missing Approval object IRI")
|
||||
l.Error("missing authorization interactingObject IRI")
|
||||
return false, nil
|
||||
|
||||
// Ensure the Approval Object is what we expect
|
||||
// Ensure the authorization object is what we expect
|
||||
// it to be, ie., it's approving the interaction
|
||||
// we need it to approve, and not something else.
|
||||
case objectIRIStr != expectObjectURIStr:
|
||||
l.Errorf(
|
||||
"resolved Approval object IRI %s was not the same as expected object %s",
|
||||
"resolved authorization interactingObject IRI %s was not the same as expected object %s",
|
||||
objectIRIStr, expectObjectURIStr,
|
||||
)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// If there's a Target set then verify it's
|
||||
// what we expect it to be, ie., it should point
|
||||
// back to the post that's being interacted with.
|
||||
targetIRIs := ap.GetTargetIRIs(approvable)
|
||||
// Ensure the authorization target is what we expect,
|
||||
// ie., it should be the status being interacted with.
|
||||
targetIRIs := ap.GetInteractionTarget(auth)
|
||||
_, targetIRIStr := extractIRI(targetIRIs)
|
||||
if targetIRIStr != "" && targetIRIStr != expectTargetURIStr {
|
||||
l.Errorf(
|
||||
"resolved Approval target IRI %s was not the same as expected target %s",
|
||||
"resolved authorization interactionTarget IRI %s was not the same as expected target %s",
|
||||
targetIRIStr, expectTargetURIStr,
|
||||
)
|
||||
return false, nil
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ func (suite *StatusTestSuite) TestDereferenceStatusWithTag() {
|
|||
// status values should be set
|
||||
suite.Equal("https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7", status.URI)
|
||||
suite.Equal("https://unknown-instance.com/users/@brand_new_person/01H641QSRS3TCXSVC10X4GPKW7", status.URL)
|
||||
suite.Equal("<p>Babe are you okay, you've hardly touched your <a href=\"https://unknown-instance.com/tags/piss\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>piss</span></a></p>", status.Content)
|
||||
suite.Equal("<p><span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span>Babe are you okay, you've hardly touched your <a href=\"https://unknown-instance.com/tags/piss\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>piss</span></a></p>", status.Content)
|
||||
suite.Equal("https://unknown-instance.com/users/brand_new_person", status.AccountURI)
|
||||
suite.False(*status.Local)
|
||||
suite.Empty(status.ContentWarning)
|
||||
|
|
|
|||
|
|
@ -42,18 +42,14 @@ func (suite *FederatingActorTestSuite) TestSendNoRemoteFollowers() {
|
|||
ctx := suite.T().Context()
|
||||
testAccount := suite.testAccounts["local_account_1"]
|
||||
testNote := testrig.NewAPNote(
|
||||
testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
testrig.URLMustParse("http://localhost:8080/@the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
time.Now(),
|
||||
"boobies",
|
||||
"",
|
||||
testrig.URLMustParse(testAccount.URI),
|
||||
[]*url.URL{testrig.URLMustParse(testAccount.FollowersURI)},
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&testrig.NewAPNoteParams{
|
||||
ID: testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
URL: testrig.URLMustParse("http://localhost:8080/@the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
CreatedAt: time.Now(),
|
||||
Content: "boobies",
|
||||
AttributedTo: testrig.URLMustParse(testAccount.URI),
|
||||
To: []*url.URL{testrig.URLMustParse(testAccount.FollowersURI)},
|
||||
},
|
||||
)
|
||||
testActivity := testrig.WrapAPNoteInCreate(testrig.URLMustParse("http://localhost:8080/whatever_some_create"), testrig.URLMustParse(testAccount.URI), time.Now(), testNote)
|
||||
|
||||
|
|
@ -98,18 +94,14 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() {
|
|||
suite.NoError(err)
|
||||
|
||||
testNote := testrig.NewAPNote(
|
||||
testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
testrig.URLMustParse("http://localhost:8080/@the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
testrig.TimeMustParse("2022-06-02T12:22:21+02:00"),
|
||||
"boobies",
|
||||
"",
|
||||
testrig.URLMustParse(testAccount.URI),
|
||||
[]*url.URL{testrig.URLMustParse(testAccount.FollowersURI)},
|
||||
nil,
|
||||
false,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
&testrig.NewAPNoteParams{
|
||||
ID: testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
URL: testrig.URLMustParse("http://localhost:8080/@the_mighty_zork/statuses/01G1TR6BADACCZWQMNF9X21TV5"),
|
||||
CreatedAt: testrig.TimeMustParse("2022-06-02T12:22:21+02:00"),
|
||||
Content: "boobies",
|
||||
AttributedTo: testrig.URLMustParse(testAccount.URI),
|
||||
To: []*url.URL{testrig.URLMustParse(testAccount.FollowersURI)},
|
||||
},
|
||||
)
|
||||
testActivity := testrig.WrapAPNoteInCreate(testrig.URLMustParse("http://localhost:8080/whatever_some_create"), testrig.URLMustParse(testAccount.URI), testrig.TimeMustParse("2022-06-02T12:22:21+02:00"), testNote)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/activity/streams/vocab"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
|
|
@ -39,7 +40,7 @@ func (f *DB) GetAccept(
|
|||
ctx context.Context,
|
||||
acceptIRI *url.URL,
|
||||
) (vocab.ActivityStreamsAccept, error) {
|
||||
approval, err := f.state.DB.GetInteractionRequestByURI(ctx, acceptIRI.String())
|
||||
approval, err := f.state.DB.GetInteractionRequestByResponseURI(ctx, acceptIRI.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -63,9 +64,9 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
|
|||
return nil
|
||||
}
|
||||
|
||||
// Ensure an activity ID is given.
|
||||
acceptID := ap.GetJSONLDId(accept)
|
||||
if acceptID == nil {
|
||||
// We need an ID.
|
||||
const text = "Accept had no id property"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
|
@ -109,7 +110,7 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
|
|||
case name == ap.ActivityLike:
|
||||
objIRI := ap.GetJSONLDId(asType)
|
||||
if objIRI == nil {
|
||||
log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name)
|
||||
log.Warnf(ctx, "missing id for inlined object %s: %s", name, acceptID)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
|
|||
case name == ap.ActivityAnnounce || ap.IsStatusable(name):
|
||||
objIRI := ap.GetJSONLDId(asType)
|
||||
if objIRI == nil {
|
||||
log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name)
|
||||
log.Warnf(ctx, "missing id for inlined object %s: %s", name, acceptID)
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -146,9 +147,38 @@ func (f *DB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) err
|
|||
return err
|
||||
}
|
||||
|
||||
// Todo: ACCEPT POLITE INLINED LIKE REQUEST.
|
||||
//
|
||||
// Implement this when we start
|
||||
// sending out polite LikeRequests.
|
||||
|
||||
// ACCEPT POLITE INLINED REPLY REQUEST
|
||||
case name == ap.ActivityReplyRequest:
|
||||
replyReq, ok := asType.(vocab.GoToSocialReplyRequest)
|
||||
if !ok {
|
||||
const text = "malformed ReplyRequest as object of Accept"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
if err := f.acceptPoliteReplyRequest(
|
||||
ctx,
|
||||
acceptID,
|
||||
accept,
|
||||
replyReq,
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Todo: ACCEPT POLITE INLINED ANNOUNCE REQUEST
|
||||
//
|
||||
// Implement this when we start
|
||||
// sending out polite AnnounceRequests.
|
||||
|
||||
// UNHANDLED
|
||||
default:
|
||||
log.Debugf(ctx, "unhandled object type: %s", name)
|
||||
log.Debugf(ctx, "unhandled object type %s: %s", name, acceptID)
|
||||
}
|
||||
|
||||
} else if object.IsIRI() {
|
||||
|
|
@ -445,8 +475,7 @@ func (f *DB) acceptStoredStatus(
|
|||
// Mark the status as approved by this URI.
|
||||
status.PendingApproval = util.Ptr(false)
|
||||
status.ApprovedByURI = approvedByURI.String()
|
||||
if err := f.state.DB.UpdateStatus(
|
||||
ctx,
|
||||
if err := f.state.DB.UpdateStatus(ctx,
|
||||
status,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
|
|
@ -543,8 +572,7 @@ func (f *DB) acceptLikeIRI(
|
|||
// Mark the fave as approved by this URI.
|
||||
fave.PendingApproval = util.Ptr(false)
|
||||
fave.ApprovedByURI = approvedByURI.String()
|
||||
if err := f.state.DB.UpdateStatusFave(
|
||||
ctx,
|
||||
if err := f.state.DB.UpdateStatusFave(ctx,
|
||||
fave,
|
||||
"pending_approval",
|
||||
"approved_by_uri",
|
||||
|
|
@ -566,6 +594,316 @@ func (f *DB) acceptLikeIRI(
|
|||
return nil
|
||||
}
|
||||
|
||||
// partialAcceptInteractionRequest represents a
|
||||
// partially-parsed accept of an interaction request
|
||||
// returned from parseAcceptInteractionRequestable.
|
||||
type partialAcceptInteractionRequest struct {
|
||||
intReqURI *url.URL
|
||||
actorURI *url.URL
|
||||
parentURI *url.URL
|
||||
instrumentURI *url.URL
|
||||
authURI *url.URL
|
||||
intReq *gtsmodel.InteractionRequest // May be nil.
|
||||
}
|
||||
|
||||
// parseAcceptInteractionRequestable does some initial parsing
|
||||
// and validation of the given Accept with inlined polite
|
||||
// interaction request (LikeRequest, ReplyRequest, AnnounceRequest).
|
||||
//
|
||||
// Will return nil, nil if there's no need for further processing.
|
||||
func (f *DB) parseAcceptInteractionRequestable(
|
||||
ctx context.Context,
|
||||
accept vocab.ActivityStreamsAccept,
|
||||
intRequestable ap.InteractionRequestable,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) (*partialAcceptInteractionRequest, error) {
|
||||
intReqURI := ap.GetJSONLDId(intRequestable)
|
||||
if intReqURI == nil {
|
||||
const text = "no id set on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// Ensure we have actor IRI on
|
||||
// the interaction requestable.
|
||||
actors := ap.GetActorIRIs(intRequestable)
|
||||
if len(actors) != 1 {
|
||||
const text = "invalid or missing actor property on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
actorURI := actors[0]
|
||||
|
||||
// Ensure we have an object URI, which
|
||||
// should point to the statusable being
|
||||
// interacted with, ie., the parent status.
|
||||
objects := ap.GetObjectIRIs(intRequestable)
|
||||
if len(objects) != 1 {
|
||||
const text = "invalid or missing object property on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
parentURI := objects[0]
|
||||
|
||||
// Ensure we have instrument, which should
|
||||
// be or point to the activity/object that
|
||||
// interacts with the parent status.
|
||||
instruments := ap.ExtractInstruments(intRequestable)
|
||||
if len(instruments) != 1 {
|
||||
const text = "invalid or missing instrument property on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
instrument := instruments[0]
|
||||
|
||||
// We just need the URI for the instrument,
|
||||
// not the whole type, which we can either
|
||||
// fetch from remote or get locally.
|
||||
var instrumentURI *url.URL
|
||||
if instrument.IsIRI() {
|
||||
instrumentURI = instrument.GetIRI()
|
||||
} else {
|
||||
t := instrument.GetType()
|
||||
if t == nil {
|
||||
const text = "nil instrument type on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
instrumentURI = ap.GetJSONLDId(t)
|
||||
}
|
||||
|
||||
// Ensure we have result URI, which should
|
||||
// point to an authorization for this interaction.
|
||||
results := ap.GetResultIRIs(accept)
|
||||
if len(results) != 1 {
|
||||
const text = "invalid or missing result property on embedded interaction request"
|
||||
return nil, gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
authURI := results[0]
|
||||
|
||||
// Check if we have a gtsmodel interaction
|
||||
// request already stored for this interaction.
|
||||
intReq, err := f.state.DB.GetInteractionRequestByInteractionURI(ctx, instrumentURI.String())
|
||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||
// Real db error.
|
||||
return nil, gtserror.Newf("db error getting interaction request: %w", err)
|
||||
}
|
||||
|
||||
if intReq == nil {
|
||||
|
||||
// No request stored for this interaction.
|
||||
// Means this is *probably* a remote interaction
|
||||
// with a remote status. Double check this.
|
||||
host := config.GetHost()
|
||||
acctDomain := config.GetAccountDomain()
|
||||
if instrumentURI.Host == host ||
|
||||
instrumentURI.Host == acctDomain ||
|
||||
intReqURI.Host == host ||
|
||||
intReqURI.Host == acctDomain {
|
||||
// Claims to be Accepting something of ours,
|
||||
// but we don't have an interaction request
|
||||
// stored. Most likely it's been deleted in
|
||||
// the meantime, or this is a mistake. Bail.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// This must be an Accept of a remote interaction
|
||||
// request. Ensure relevance of this message by
|
||||
// checking that receiver follows requester.
|
||||
following, err := f.state.DB.IsFollowing(
|
||||
ctx,
|
||||
receivingAcct.ID,
|
||||
requestingAcct.ID,
|
||||
)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("db error checking following: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !following {
|
||||
// If we don't follow this person, and
|
||||
// they're not Accepting something we
|
||||
// created, then we don't care.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Request stored for this interaction URI.
|
||||
//
|
||||
// Note: this path is not actually possible until v0.21.0,
|
||||
// because we don't send out polite requests yet in v0.20.0.
|
||||
|
||||
// If the request is already accepted,
|
||||
// we don't need to do anything at all.
|
||||
if intReq.IsAccepted() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// The person doing the Accept must be the
|
||||
// same as the target of the interaction request.
|
||||
if intReq.TargetAccountID != requestingAcct.ID {
|
||||
const text = "cannot Accept interaction request on another actor's behalf"
|
||||
return nil, gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// The stored interaction request and the inlined
|
||||
// interaction request must have the same target status.
|
||||
if intReq.TargetStatus.URI != parentURI.String() {
|
||||
const text = "Accept interaction request mismatched object URI"
|
||||
return nil, gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// The stored interaction request and the inlined
|
||||
// interaction request must have the same URI.
|
||||
if intReq.InteractionRequestURI != intReqURI.String() {
|
||||
const text = "Accept interaction request mismatched id"
|
||||
return nil, gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the things.
|
||||
return &partialAcceptInteractionRequest{
|
||||
intReqURI: intReqURI,
|
||||
actorURI: actorURI,
|
||||
parentURI: parentURI,
|
||||
instrumentURI: instrumentURI,
|
||||
authURI: authURI,
|
||||
intReq: intReq, // May be nil.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// acceptPoliteReplyRequest handles the Accept of a polite ReplyRequest,
|
||||
// ie., something that looks like this:
|
||||
//
|
||||
// {
|
||||
// "@context": [
|
||||
// "https://www.w3.org/ns/activitystreams",
|
||||
// "https://gotosocial.org/ns"
|
||||
// ],
|
||||
// "type": "Accept",
|
||||
// "to": "https://example.com/users/bob",
|
||||
// "id": "https://example.com/users/alice/activities/1234",
|
||||
// "actor": "https://example.com/users/alice",
|
||||
// "object": {
|
||||
// "type": "ReplyRequest",
|
||||
// "id": "https://example.com/users/bob/interaction_requests/12345",
|
||||
// "actor": "https://example.com/users/bob",
|
||||
// "object": "https://example.com/users/alice/statuses/1",
|
||||
// "instrument": "https://example.org/users/bob/statuses/12345"
|
||||
// },
|
||||
// "result": "https://example.com/users/alice/authorizations/1"
|
||||
// }
|
||||
func (f *DB) acceptPoliteReplyRequest(
|
||||
ctx context.Context,
|
||||
acceptID *url.URL,
|
||||
accept vocab.ActivityStreamsAccept,
|
||||
replyRequest vocab.GoToSocialReplyRequest,
|
||||
receivingAcct *gtsmodel.Account,
|
||||
requestingAcct *gtsmodel.Account,
|
||||
) error {
|
||||
// Parse out the Accept and
|
||||
// embedded interaction requestable.
|
||||
partial, err := f.parseAcceptInteractionRequestable(
|
||||
ctx,
|
||||
accept,
|
||||
replyRequest,
|
||||
receivingAcct,
|
||||
requestingAcct,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if partial == nil {
|
||||
// Nothing to do!
|
||||
return nil
|
||||
}
|
||||
|
||||
if partial.intReq == nil {
|
||||
// This is a remote accept of a remote reply.
|
||||
//
|
||||
// Process dereferencing etc asynchronously, leaving
|
||||
// the interaction request as nil. We don't need to
|
||||
// create an int req for remote accepts of remote
|
||||
// replies, we can just validate + store the auth URI.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityReplyRequest,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
APIRI: partial.authURI,
|
||||
APObject: partial.instrumentURI,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// We already have a request stored for this interaction.
|
||||
//
|
||||
// Note: this path is not actually possible until v0.21.0,
|
||||
// because we don't send out polite requests yet in v0.20.0.
|
||||
|
||||
// Make sure the stored interaction request
|
||||
// lines up with the Accept ReplyRequest.
|
||||
if partial.intReq.InteractionType != gtsmodel.InteractionReply {
|
||||
const text = "Accept ReplyRequest targets interaction request that isn't of type Reply"
|
||||
return gtserror.NewErrorBadRequest(errors.New(text), text)
|
||||
}
|
||||
|
||||
// The stored reply must be the same as
|
||||
// the instrument of the ReplyRequest.
|
||||
reply := partial.intReq.Reply
|
||||
if reply.URI != partial.instrumentURI.String() {
|
||||
const text = "Accept ReplyRequest mismatched instrument URI"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// The actor of the stored reply must be the
|
||||
// same as the actor of the ReplyRequest.
|
||||
if reply.AccountURI != partial.actorURI.String() {
|
||||
const text = "Accept ReplyRequest mismatched actor URI"
|
||||
return gtserror.NewErrorForbidden(errors.New(text), text)
|
||||
}
|
||||
|
||||
// This all looks good, we can update the
|
||||
// interaction request and stored reply.
|
||||
unlock := f.state.FedLocks.Lock(partial.intReq.InteractionURI)
|
||||
defer unlock()
|
||||
|
||||
authURIStr := partial.authURI.String()
|
||||
partial.intReq.AcceptedAt = time.Now()
|
||||
partial.intReq.AuthorizationURI = authURIStr
|
||||
partial.intReq.ResponseURI = acceptID.String()
|
||||
if err := f.state.DB.UpdateInteractionRequest(
|
||||
ctx, partial.intReq,
|
||||
"accepted_at",
|
||||
"authorization_uri",
|
||||
"response_uri",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating interaction request: %w", err)
|
||||
}
|
||||
|
||||
reply.ApprovedByURI = authURIStr
|
||||
reply.PendingApproval = util.Ptr(false)
|
||||
if err := f.state.DB.UpdateStatus(
|
||||
ctx, reply,
|
||||
"approved_by_uri",
|
||||
"pending_approval",
|
||||
); err != nil {
|
||||
return gtserror.Newf("db error updating status: %w", err)
|
||||
}
|
||||
|
||||
// Handle any remaining side effects in the processor.
|
||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||
APObjectType: ap.ActivityReplyRequest,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
APIRI: partial.authURI,
|
||||
APObject: partial.instrumentURI,
|
||||
GTSModel: partial.intReq,
|
||||
Receiving: receivingAcct,
|
||||
Requesting: requestingAcct,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// approvedByURI extracts the appropriate *url.URL
|
||||
// to use as an interaction's approvedBy value by
|
||||
// checking to see if the Accept has a result URL set.
|
||||
|
|
@ -577,11 +915,8 @@ func (f *DB) acceptLikeIRI(
|
|||
// Error is only returned if the result URI is set
|
||||
// but the host differs from the Accept ID host.
|
||||
//
|
||||
// TODO: This function should be updated at some point
|
||||
// to check for inlined result type, and see if type is
|
||||
// a LikeApproval, ReplyApproval, or AnnounceApproval,
|
||||
// and check the attributedTo, object, and target of
|
||||
// the approval as well. But this'll do for now.
|
||||
// TODO: This function could be updated at some
|
||||
// point to check for inlined result type.
|
||||
func approvedByURI(
|
||||
acceptID *url.URL,
|
||||
accept vocab.ActivityStreamsAccept,
|
||||
|
|
@ -623,11 +958,7 @@ func approvedByURI(
|
|||
|
||||
if resultIRI.Host != acceptID.Host {
|
||||
// What the boobs is this?
|
||||
err := fmt.Errorf(
|
||||
"host of result %s differed from host of Accept %s",
|
||||
resultIRI, accept,
|
||||
)
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("host of result %s differed from host of Accept %s", resultIRI, accept)
|
||||
}
|
||||
|
||||
// Use the result IRI we've been
|
||||
|
|
|
|||
120
internal/federation/federatingdb/accept_test.go
Normal file
120
internal/federation/federatingdb/accept_test.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"code.superseriousbusiness.org/activity/streams/vocab"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/messages"
|
||||
"code.superseriousbusiness.org/gotosocial/testrig"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
const (
|
||||
rMediaPath = "../../../testrig/media"
|
||||
rTemplatePath = "../../../web/template"
|
||||
)
|
||||
|
||||
type AcceptTestSuite struct {
|
||||
FederatingDBTestSuite
|
||||
}
|
||||
|
||||
func (suite *AcceptTestSuite) TestAcceptRemoteReplyRequest() {
|
||||
// Accept of a reply by
|
||||
// brand_new_person to foss_satan.
|
||||
const acceptJSON = `{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://gotosocial.org/ns"
|
||||
],
|
||||
"type": "Accept",
|
||||
"to": "https://unknown-instance.com/users/brand_new_person",
|
||||
"cc": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"id": "http://fossbros-anonymous.io/users/foss_satan/accepts/1234",
|
||||
"actor": "http://fossbros-anonymous.io/users/foss_satan",
|
||||
"object": {
|
||||
"type": "ReplyRequest",
|
||||
"id": "https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7/replyRequest",
|
||||
"actor": "https://unknown-instance.com/users/brand_new_person",
|
||||
"object": "http://fossbros-anonymous.io/users/foss_satan/statuses/01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
"instrument": "https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7"
|
||||
},
|
||||
"result": "http://fossbros-anonymous.io/users/foss_satan/authorizations/1234"
|
||||
}`
|
||||
|
||||
// The accept will be delivered by foss_satan to zork.
|
||||
ctx := createTestContext(
|
||||
suite.T(),
|
||||
suite.testAccounts["local_account_1"],
|
||||
suite.testAccounts["remote_account_1"],
|
||||
)
|
||||
|
||||
// Have zork follow foss_satan for this test,
|
||||
// else the message will be scattered unto the four winds.
|
||||
follow := >smodel.Follow{
|
||||
ID: "01K4STEH5NWAXBZ4TFNGQQQ984",
|
||||
CreatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
UpdatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
TargetAccountID: "01F8MH5ZK5VRH73AKHQM6Y9VNX",
|
||||
URI: "http://localhost:8080/users/the_mighty_zork/follow/01G1TK3PQKFW1BQZ9WVYRTFECK",
|
||||
}
|
||||
if err := suite.state.DB.PutFollow(ctx, follow); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// Parse accept into vocab.Type.
|
||||
t, err := ap.DecodeType(ctx, io.NopCloser(bytes.NewBufferString(acceptJSON)))
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
accept := t.(vocab.ActivityStreamsAccept)
|
||||
|
||||
// Process the accept.
|
||||
if err := suite.federatingDB.Accept(ctx, accept); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be an accept msg
|
||||
// heading to the processor now.
|
||||
msg, ok := suite.state.Workers.Federator.Queue.PopCtx(ctx)
|
||||
if !ok {
|
||||
suite.FailNow("no message in queue")
|
||||
}
|
||||
|
||||
suite.EqualValues(
|
||||
&messages.FromFediAPI{
|
||||
APObjectType: "ReplyRequest",
|
||||
APActivityType: "Accept",
|
||||
APIRI: testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/authorizations/1234"),
|
||||
APObject: testrig.URLMustParse("https://unknown-instance.com/users/brand_new_person/statuses/01H641QSRS3TCXSVC10X4GPKW7"),
|
||||
Requesting: suite.testAccounts["remote_account_1"],
|
||||
Receiving: suite.testAccounts["local_account_1"],
|
||||
},
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
func TestAcceptTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(AcceptTestSuite))
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ func (f *DB) Block(ctx context.Context, blockable vocab.ActivityStreamsBlock) er
|
|||
requesting := activityContext.requestingAcct
|
||||
receiving := activityContext.receivingAcct
|
||||
|
||||
if receiving.IsMoving() {
|
||||
if requesting.IsMoving() {
|
||||
// A Moving account
|
||||
// can't do this.
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -281,43 +281,18 @@ func (f *DB) createStatusable(
|
|||
statusable ap.Statusable,
|
||||
forwarded bool,
|
||||
) error {
|
||||
// Check whether this status is both
|
||||
// relevant, and doesn't look like spam.
|
||||
err := f.spamFilter.StatusableOK(ctx,
|
||||
receiver,
|
||||
requester,
|
||||
statusable,
|
||||
)
|
||||
// Check for spam / relevance.
|
||||
ok, err := f.statusableOK(ctx, receiver, requester, statusable)
|
||||
if err != nil {
|
||||
// Error already
|
||||
// wrapped.
|
||||
return err
|
||||
}
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// No problem!
|
||||
|
||||
case gtserror.IsNotRelevant(err):
|
||||
// This case is quite common if a remote (Mastodon)
|
||||
// instance forwards a message to us which is a reply
|
||||
// from someone else to a status we've also replied to.
|
||||
//
|
||||
// It does this to try to ensure thread completion, but
|
||||
// we have our own thread fetching mechanism anyway.
|
||||
log.Debugf(ctx, "status %s is not relevant to receiver (%v); dropping it",
|
||||
ap.GetJSONLDId(statusable), err,
|
||||
)
|
||||
if !ok {
|
||||
// Not relevant / spam.
|
||||
// Already logged.
|
||||
return nil
|
||||
|
||||
case gtserror.IsSpam(err):
|
||||
// Log this at a higher level so admins can
|
||||
// gauge how much spam is being sent to them.
|
||||
//
|
||||
// TODO: add Prometheus metrics for this.
|
||||
log.Infof(ctx, "status %s looked like spam (%v); dropping it",
|
||||
ap.GetJSONLDId(statusable), err,
|
||||
)
|
||||
return nil
|
||||
|
||||
default:
|
||||
// A real error has occurred.
|
||||
return gtserror.Newf("error checking relevancy/spam: %w", err)
|
||||
}
|
||||
|
||||
// If we do have a forward, we should ignore the content
|
||||
|
|
|
|||
572
internal/federation/federatingdb/interactionrequest.go
Normal file
572
internal/federation/federatingdb/interactionrequest.go
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
// 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
|
||||
}
|
||||
289
internal/federation/federatingdb/interactionrequest_test.go
Normal file
289
internal/federation/federatingdb/interactionrequest_test.go
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
// 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_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.superseriousbusiness.org/activity/streams"
|
||||
"code.superseriousbusiness.org/activity/streams/vocab"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/testrig"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type InteractionRequestTestSuite struct {
|
||||
FederatingDBTestSuite
|
||||
}
|
||||
|
||||
func (suite *InteractionRequestTestSuite) intReq(
|
||||
receiving *gtsmodel.Account,
|
||||
requesting *gtsmodel.Account,
|
||||
jsonStr string,
|
||||
dbF func(ctx context.Context, req vocab.Type) error,
|
||||
) error {
|
||||
ctx := createTestContext(suite.T(), receiving, requesting)
|
||||
|
||||
raw := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
t, err := streams.ToType(ctx, raw)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
return dbF(ctx, t)
|
||||
}
|
||||
|
||||
func (suite *InteractionRequestTestSuite) TestReplyRequest() {
|
||||
var (
|
||||
ctx = suite.T().Context()
|
||||
receiving = suite.testAccounts["admin_account"]
|
||||
requesting = suite.testAccounts["remote_account_1"]
|
||||
testStatus = suite.testStatuses["admin_account_status_1"]
|
||||
intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
jsonStr = `{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://gotosocial.org/ns"
|
||||
],
|
||||
"type": "ReplyRequest",
|
||||
"id": "` + intReqURI + `",
|
||||
"actor": "` + requesting.URI + `",
|
||||
"object": "` + testStatus.URI + `",
|
||||
"to": "` + receiving.URI + `",
|
||||
"instrument": {
|
||||
"attributedTo": "` + requesting.URI + `",
|
||||
"cc": "` + requesting.FollowersURI + `",
|
||||
"content": "\u003cp\u003ethis is a reply!\u003c/p\u003e",
|
||||
"id": "` + intURI + `",
|
||||
"inReplyTo": "` + testStatus.URI + `",
|
||||
"tag": {
|
||||
"href": "` + receiving.URI + `",
|
||||
"name": "@` + receiving.Username + `@localhost:8080",
|
||||
"type": "Mention"
|
||||
},
|
||||
"to": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"type": "Note"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
suite.T().Logf("testing reply request:\n\n%s", jsonStr)
|
||||
|
||||
// Call the federatingDB function.
|
||||
err := suite.intReq(
|
||||
receiving,
|
||||
requesting,
|
||||
jsonStr,
|
||||
func(ctx context.Context, req vocab.Type) error {
|
||||
replyReq := req.(vocab.GoToSocialReplyRequest)
|
||||
return suite.federatingDB.ReplyRequest(ctx, replyReq)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be an interaction request in the DB now.
|
||||
var intReq *gtsmodel.InteractionRequest
|
||||
if !testrig.WaitFor(func() bool {
|
||||
intReq, err = suite.state.DB.GetInteractionRequestByInteractionURI(ctx, intURI)
|
||||
return err == nil && intReq != nil
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for int req to appear in the db")
|
||||
}
|
||||
suite.Equal(testStatus.ID, intReq.TargetStatusID)
|
||||
suite.Equal(receiving.ID, intReq.TargetAccountID)
|
||||
suite.Equal(requesting.ID, intReq.InteractingAccountID)
|
||||
suite.Equal(intReqURI, intReq.InteractionRequestURI)
|
||||
suite.Equal(intURI, intReq.InteractionURI)
|
||||
suite.Equal(gtsmodel.InteractionReply, intReq.InteractionType)
|
||||
|
||||
// Should be a message heading to the processor.
|
||||
msg, _ := suite.getFederatorMsg(5 * time.Second)
|
||||
suite.Equal(ap.ActivityCreate, msg.APActivityType)
|
||||
suite.Equal(ap.ActivityReplyRequest, msg.APObjectType)
|
||||
suite.NotNil(msg.GTSModel)
|
||||
suite.NotNil(msg.APObject)
|
||||
suite.NotNil(msg.Receiving)
|
||||
suite.NotNil(msg.Requesting)
|
||||
}
|
||||
|
||||
func (suite *InteractionRequestTestSuite) TestLikeRequest() {
|
||||
var (
|
||||
ctx = suite.T().Context()
|
||||
receiving = suite.testAccounts["admin_account"]
|
||||
requesting = suite.testAccounts["remote_account_1"]
|
||||
testStatus = suite.testStatuses["admin_account_status_1"]
|
||||
intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
jsonStr = `{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://gotosocial.org/ns"
|
||||
],
|
||||
"type": "LikeRequest",
|
||||
"id": "` + intReqURI + `",
|
||||
"actor": "` + requesting.URI + `",
|
||||
"object": "` + testStatus.URI + `",
|
||||
"to": "` + receiving.URI + `",
|
||||
"instrument": {
|
||||
"id": "` + intURI + `",
|
||||
"object": "` + testStatus.URI + `",
|
||||
"actor": "` + requesting.URI + `",
|
||||
"to": "` + receiving.URI + `",
|
||||
"type": "Like"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
suite.T().Logf("testing like request:\n\n%s", jsonStr)
|
||||
|
||||
// Call the federatingDB function.
|
||||
err := suite.intReq(
|
||||
receiving,
|
||||
requesting,
|
||||
jsonStr,
|
||||
func(ctx context.Context, req vocab.Type) error {
|
||||
likeReq := req.(vocab.GoToSocialLikeRequest)
|
||||
return suite.federatingDB.LikeRequest(ctx, likeReq)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be an interaction request in the DB now.
|
||||
var intReq *gtsmodel.InteractionRequest
|
||||
if !testrig.WaitFor(func() bool {
|
||||
intReq, err = suite.state.DB.GetInteractionRequestByInteractionURI(ctx, intURI)
|
||||
return err == nil && intReq != nil
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for int req to appear in the db")
|
||||
}
|
||||
suite.Equal(testStatus.ID, intReq.TargetStatusID)
|
||||
suite.Equal(receiving.ID, intReq.TargetAccountID)
|
||||
suite.Equal(requesting.ID, intReq.InteractingAccountID)
|
||||
suite.Equal(intReqURI, intReq.InteractionRequestURI)
|
||||
suite.Equal(intURI, intReq.InteractionURI)
|
||||
suite.Equal(gtsmodel.InteractionLike, intReq.InteractionType)
|
||||
|
||||
// The like should be in the DB now (unapproved).
|
||||
var statusFave *gtsmodel.StatusFave
|
||||
if !testrig.WaitFor(func() bool {
|
||||
statusFave, err = suite.state.DB.GetStatusFaveByURI(ctx, intURI)
|
||||
return err == nil && intReq != nil
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for fave to appear in the db")
|
||||
}
|
||||
suite.Equal(requesting.ID, statusFave.AccountID)
|
||||
suite.Equal(receiving.ID, statusFave.TargetAccountID)
|
||||
suite.Equal(testStatus.ID, statusFave.StatusID)
|
||||
suite.Equal(intURI, statusFave.URI)
|
||||
suite.True(*statusFave.PendingApproval)
|
||||
suite.Empty(statusFave.ApprovedByURI)
|
||||
|
||||
// Should be a message heading to the processor.
|
||||
msg, _ := suite.getFederatorMsg(5 * time.Second)
|
||||
suite.Equal(ap.ActivityCreate, msg.APActivityType)
|
||||
suite.Equal(ap.ActivityLikeRequest, msg.APObjectType)
|
||||
suite.NotNil(msg.GTSModel)
|
||||
suite.NotNil(msg.Receiving)
|
||||
suite.NotNil(msg.Requesting)
|
||||
}
|
||||
|
||||
func (suite *InteractionRequestTestSuite) TestAnnounceRequest() {
|
||||
var (
|
||||
ctx = suite.T().Context()
|
||||
receiving = suite.testAccounts["admin_account"]
|
||||
requesting = suite.testAccounts["remote_account_1"]
|
||||
testStatus = suite.testStatuses["admin_account_status_1"]
|
||||
intReqURI = "http://fossbros-anonymous.io/requests/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
intURI = "http://fossbros-anonymous.io/users/foss_satan/statuses/87fb1478-ac46-406a-8463-96ce05645219"
|
||||
jsonStr = `{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://gotosocial.org/ns"
|
||||
],
|
||||
"type": "AnnounceRequest",
|
||||
"id": "` + intReqURI + `",
|
||||
"actor": "` + requesting.URI + `",
|
||||
"object": "` + testStatus.URI + `",
|
||||
"to": "` + receiving.URI + `",
|
||||
"instrument": {
|
||||
"id": "` + intURI + `",
|
||||
"object": "` + testStatus.URI + `",
|
||||
"actor": "` + requesting.URI + `",
|
||||
"to": "` + requesting.FollowersURI + `",
|
||||
"cc": "` + receiving.URI + `",
|
||||
"type": "Announce"
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
suite.T().Logf("testing announce request:\n\n%s", jsonStr)
|
||||
|
||||
// Call the federatingDB function.
|
||||
err := suite.intReq(
|
||||
receiving,
|
||||
requesting,
|
||||
jsonStr,
|
||||
func(ctx context.Context, req vocab.Type) error {
|
||||
announceReq := req.(vocab.GoToSocialAnnounceRequest)
|
||||
return suite.federatingDB.AnnounceRequest(ctx, announceReq)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
// There should be an interaction request in the DB now.
|
||||
var intReq *gtsmodel.InteractionRequest
|
||||
if !testrig.WaitFor(func() bool {
|
||||
intReq, err = suite.state.DB.GetInteractionRequestByInteractionURI(ctx, intURI)
|
||||
return err == nil && intReq != nil
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for int req to appear in the db")
|
||||
}
|
||||
suite.Equal(testStatus.ID, intReq.TargetStatusID)
|
||||
suite.Equal(receiving.ID, intReq.TargetAccountID)
|
||||
suite.Equal(requesting.ID, intReq.InteractingAccountID)
|
||||
suite.Equal(intReqURI, intReq.InteractionRequestURI)
|
||||
suite.Equal(intURI, intReq.InteractionURI)
|
||||
suite.Equal(gtsmodel.InteractionAnnounce, intReq.InteractionType)
|
||||
|
||||
// Should be a message heading to the processor.
|
||||
msg, _ := suite.getFederatorMsg(5 * time.Second)
|
||||
suite.Equal(ap.ActivityCreate, msg.APActivityType)
|
||||
suite.Equal(ap.ActivityAnnounceRequest, msg.APObjectType)
|
||||
suite.NotNil(msg.GTSModel)
|
||||
suite.NotNil(msg.Receiving)
|
||||
suite.NotNil(msg.Requesting)
|
||||
}
|
||||
|
||||
func TestInteractionRequestTestSuite(t *testing.T) {
|
||||
suite.Run(t, &InteractionRequestTestSuite{})
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ func (f *DB) Like(ctx context.Context, likeable vocab.ActivityStreamsLike) error
|
|||
requesting := activityContext.requestingAcct
|
||||
receiving := activityContext.receivingAcct
|
||||
|
||||
if receiving.IsMoving() {
|
||||
if requesting.IsMoving() {
|
||||
// A Moving account
|
||||
// can't do this.
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/messages"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/uris"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
func (f *DB) Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error {
|
||||
|
|
@ -305,21 +306,24 @@ func (f *DB) rejectStatusIRI(
|
|||
InteractingAccountID: receivingAcct.ID,
|
||||
InteractingAccount: receivingAcct,
|
||||
InteractionURI: status.URI,
|
||||
URI: activityID,
|
||||
Polite: util.Ptr(false),
|
||||
ResponseURI: activityID,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
|
||||
if apObjectType == ap.ObjectNote {
|
||||
// Reply.
|
||||
req.InteractionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.ReplyRequestSuffix)
|
||||
req.InteractionType = gtsmodel.InteractionReply
|
||||
req.StatusID = status.InReplyToID
|
||||
req.Status = status.InReplyTo
|
||||
req.TargetStatusID = status.InReplyToID
|
||||
req.TargetStatus = status.InReplyTo
|
||||
req.Reply = status
|
||||
} else {
|
||||
// Announce.
|
||||
req.InteractionRequestURI = gtsmodel.ForwardCompatibleInteractionRequestURI(status.URI, gtsmodel.AnnounceRequestSuffix)
|
||||
req.InteractionType = gtsmodel.InteractionAnnounce
|
||||
req.StatusID = status.BoostOfID
|
||||
req.Status = status.BoostOf
|
||||
req.TargetStatusID = status.BoostOfID
|
||||
req.TargetStatus = status.BoostOf
|
||||
req.Announce = status
|
||||
}
|
||||
|
||||
|
|
@ -331,8 +335,8 @@ func (f *DB) rejectStatusIRI(
|
|||
case req.IsRejected():
|
||||
// Interaction has already been rejected. Just
|
||||
// update to this Reject URI and then return early.
|
||||
req.URI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
|
||||
req.ResponseURI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req, "response_uri"); err != nil {
|
||||
err := gtserror.Newf("db error updating interaction request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -343,11 +347,11 @@ func (f *DB) rejectStatusIRI(
|
|||
// Rejected, even if previously Accepted.
|
||||
req.AcceptedAt = time.Time{}
|
||||
req.RejectedAt = time.Now()
|
||||
req.URI = activityID
|
||||
req.ResponseURI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req,
|
||||
"accepted_at",
|
||||
"rejected_at",
|
||||
"uri",
|
||||
"response_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating interaction request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
|
|
@ -430,16 +434,18 @@ func (f *DB) rejectLikeIRI(
|
|||
// No interaction request existed yet for this
|
||||
// fave, create a pre-rejected request now.
|
||||
req = >smodel.InteractionRequest{
|
||||
ID: id.NewULID(),
|
||||
TargetAccountID: requestingAcct.ID,
|
||||
TargetAccount: requestingAcct,
|
||||
InteractingAccountID: receivingAcct.ID,
|
||||
InteractingAccount: receivingAcct,
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Like: fave,
|
||||
URI: activityID,
|
||||
RejectedAt: time.Now(),
|
||||
ID: id.NewULID(),
|
||||
TargetAccountID: requestingAcct.ID,
|
||||
TargetAccount: requestingAcct,
|
||||
InteractingAccountID: receivingAcct.ID,
|
||||
InteractingAccount: receivingAcct,
|
||||
InteractionRequestURI: gtsmodel.ForwardCompatibleInteractionRequestURI(fave.URI, gtsmodel.LikeRequestSuffix),
|
||||
InteractionURI: fave.URI,
|
||||
InteractionType: gtsmodel.InteractionLike,
|
||||
Polite: util.Ptr(false),
|
||||
Like: fave,
|
||||
ResponseURI: activityID,
|
||||
RejectedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := f.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||
|
|
@ -450,8 +456,8 @@ func (f *DB) rejectLikeIRI(
|
|||
case req.IsRejected():
|
||||
// Interaction has already been rejected. Just
|
||||
// update to this Reject URI and then return early.
|
||||
req.URI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req, "uri"); err != nil {
|
||||
req.ResponseURI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req, "response_uri"); err != nil {
|
||||
err := gtserror.Newf("db error updating interaction request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
|
@ -462,11 +468,11 @@ func (f *DB) rejectLikeIRI(
|
|||
// Rejected, even if previously Accepted.
|
||||
req.AcceptedAt = time.Time{}
|
||||
req.RejectedAt = time.Now()
|
||||
req.URI = activityID
|
||||
req.ResponseURI = activityID
|
||||
if err := f.state.DB.UpdateInteractionRequest(ctx, req,
|
||||
"accepted_at",
|
||||
"rejected_at",
|
||||
"uri",
|
||||
"response_uri",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating interaction request: %w", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"code.superseriousbusiness.org/gotosocial/internal/ap"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/config"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtscontext"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtserror"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/gtsmodel"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/id"
|
||||
"code.superseriousbusiness.org/gotosocial/internal/log"
|
||||
|
|
@ -110,11 +111,9 @@ func (f *DB) NewID(ctx context.Context, t vocab.Type) (idURL *url.URL, err error
|
|||
// based on actor (i.e. followER not the followEE).
|
||||
if uri := ap.GetActorIRIs(follow); len(uri) == 1 {
|
||||
if actorAccount, err := f.state.DB.GetAccountByURI(ctx, uri[0].String()); err == nil {
|
||||
newID, err := id.NewRandomULID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return url.Parse(uris.GenerateURIForFollow(actorAccount.Username, newID))
|
||||
newID := id.NewRandomULID()
|
||||
uri := uris.GenerateURIForFollow(actorAccount.Username, newID)
|
||||
return url.Parse(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,3 +233,53 @@ func (s serialize) String() string {
|
|||
|
||||
return byteutil.B2S(b)
|
||||
}
|
||||
|
||||
// statusableOK is a util function to check if
|
||||
// the given statusable is "ok" in terms of being
|
||||
// relevant to the receiver, and passing spam checks.
|
||||
func (f *DB) statusableOK(
|
||||
ctx context.Context,
|
||||
receiver *gtsmodel.Account,
|
||||
requester *gtsmodel.Account,
|
||||
statusable ap.Statusable,
|
||||
) (bool, error) {
|
||||
// Check whether this status is both
|
||||
// relevant, and doesn't look like spam.
|
||||
err := f.spamFilter.StatusableOK(ctx,
|
||||
receiver,
|
||||
requester,
|
||||
statusable,
|
||||
)
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// No problem!
|
||||
return true, nil
|
||||
|
||||
case gtserror.IsNotRelevant(err):
|
||||
// This case is quite common if a remote (Mastodon)
|
||||
// instance forwards a message to us which is a reply
|
||||
// from someone else to a status we've also replied to.
|
||||
//
|
||||
// It does this to try to ensure thread completion, but
|
||||
// we have our own thread fetching mechanism anyway.
|
||||
log.Debugf(ctx, "status %s is not relevant to receiver (%v); dropping it",
|
||||
ap.GetJSONLDId(statusable), err,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
case gtserror.IsSpam(err):
|
||||
// Log this at a higher level so admins can
|
||||
// gauge how much spam is being sent to them.
|
||||
//
|
||||
// TODO: add Prometheus metrics for this.
|
||||
log.Infof(ctx, "status %s looked like spam (%v); dropping it",
|
||||
ap.GetJSONLDId(statusable), err,
|
||||
)
|
||||
return false, nil
|
||||
|
||||
default:
|
||||
// A real error has occurred.
|
||||
return false, gtserror.Newf("error checking relevancy/spam: %w", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,9 @@ func NewFederator(
|
|||
federatingDB.Announce,
|
||||
federatingDB.Move,
|
||||
federatingDB.Flag,
|
||||
federatingDB.LikeRequest,
|
||||
federatingDB.ReplyRequest,
|
||||
federatingDB.AnnounceRequest,
|
||||
},
|
||||
}
|
||||
actor := newFederatingActor(f, f, federatingDB, clock)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue