start hacking away at the federating db

This commit is contained in:
tsmethurst 2023-05-22 18:29:32 +02:00
parent ea1bbacf4b
commit 7f3e946c89
10 changed files with 279 additions and 140 deletions

View file

@ -19,32 +19,37 @@
import (
"errors"
"strings"
"github.com/gin-gonic/gin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
"github.com/superseriousbusiness/gotosocial/internal/log"
)
var errMalformed = errors.New("inbox POST could not be processed as an ActivityPub request; request may be malformed or insufficient") // mood
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
// Eg., POST to https://example.org/users/whatever/inbox.
func (m *Module) InboxPOSTHandler(c *gin.Context) {
// usernames on our instance are always lowercase
requestedUsername := strings.ToLower(c.Param(UsernameKey))
if requestedUsername == "" {
err := errors.New("no username specified in request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
handled, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request)
if err != nil {
// If a WithCode was returned to us, we can be nice and
// try to return something informative to the caller.
if withCode := new(gtserror.WithCode); errors.As(err, withCode) {
apiutil.ErrorHandler(c, *withCode, m.processor.InstanceGetV1)
return
}
if posted, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil {
if withCode, ok := err.(gtserror.WithCode); ok {
apiutil.ErrorHandler(c, withCode, m.processor.InstanceGetV1)
} else {
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
// Something else went wrong, log the error but don't
// return it to the caller, to avoid leaking internals.
log.WithContext(c.Request.Context()).Errorf("returning Bad Request to caller, err was: %q", err)
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err), m.processor.InstanceGetV1)
return
}
} else if !posted {
err := errors.New("unable to process request")
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
if !handled {
// Wasn't a type we could understand; we should let the caller know this.
withCode := gtserror.NewErrorBadRequest(errMalformed, errMalformed.Error())
apiutil.ErrorHandler(c, withCode, m.processor.InstanceGetV1)
}
}

View file

@ -21,6 +21,8 @@
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -35,6 +37,7 @@
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/testrig"
@ -44,11 +47,79 @@ type InboxPostTestSuite struct {
UserStandardTestSuite
}
func (suite *InboxPostTestSuite) TestPostBlock() {
blockingAccount := suite.testAccounts["remote_account_1"]
blockedAccount := suite.testAccounts["local_account_1"]
blockURI := testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3")
func (suite *InboxPostTestSuite) inboxPost(
activity pub.Activity,
requestingAccount *gtsmodel.Account,
targetAccount *gtsmodel.Account,
expectedHTTPStatus int,
expectedBody string,
) {
var (
recorder = httptest.NewRecorder()
ctx, _ = testrig.CreateGinTestContext(recorder, nil)
)
// Prepare the requst body bytes.
bodyI, err := ap.Serialize(activity)
if err != nil {
suite.FailNow(err.Error())
}
b, err := json.Marshal(bodyI)
if err != nil {
suite.FailNow(err.Error())
}
// Prepare signature headers for this Activity.
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(
activity,
requestingAccount.PublicKeyURI,
requestingAccount.PrivateKey,
testrig.URLMustParse(targetAccount.InboxURI),
)
// Put the request together.
ctx.AddParam(users.UsernameKey, targetAccount.Username)
ctx.Request = httptest.NewRequest(http.MethodPost, targetAccount.InboxURI, bytes.NewReader(b))
ctx.Request.Header.Set("Signature", signature)
ctx.Request.Header.Set("Date", dateHeader)
ctx.Request.Header.Set("Digest", digestHeader)
ctx.Request.Header.Set("Content-Type", "application/activity+json")
// Pass the context through signature check
// middleware first to populate it appropriately.
suite.signatureCheck(ctx)
// Trigger the function being tested.
suite.userModule.InboxPOSTHandler(ctx)
// Read the result.
result := recorder.Result()
defer result.Body.Close()
b, err = io.ReadAll(result.Body)
if err != nil {
suite.FailNow(err.Error())
}
errs := gtserror.MultiError{}
// Check expected code + body.
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode))
}
// If we got an expected body, return early.
if expectedBody != "" && string(b) != expectedBody {
errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b)))
}
if err := errs.Combine(); err != nil {
suite.FailNow("", "%v (body %s)", err, string(b))
}
}
func (suite *InboxPostTestSuite) newBlock(blockID string, blockingAccount *gtsmodel.Account, blockedAccount *gtsmodel.Account) vocab.ActivityStreamsBlock {
block := streams.NewActivityStreamsBlock()
// set the actor property to the block-ing account's URI
@ -59,7 +130,7 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
// set the ID property to the blocks's URI
idProp := streams.NewJSONLDIdProperty()
idProp.Set(blockURI)
idProp.Set(testrig.URLMustParse(blockID))
block.SetJSONLDId(idProp)
// set the object property to the target account's URI
@ -74,57 +145,32 @@ func (suite *InboxPostTestSuite) TestPostBlock() {
toProp.AppendIRI(toIRI)
block.SetActivityStreamsTo(toProp)
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
return block
}
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
bodyI, err := ap.Serialize(block)
suite.NoError(err)
func (suite *InboxPostTestSuite) TestPostBlock() {
var (
requestingAccount = suite.testAccounts["remote_account_1"]
targetAccount = suite.testAccounts["local_account_1"]
activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3"
)
bodyJson, err := json.Marshal(bodyI)
suite.NoError(err)
body := bytes.NewReader(bodyJson)
tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media"))
federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager)
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager)
userModule := users.New(processor)
suite.NoError(processor.Start())
// setup request
recorder := httptest.NewRecorder()
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting
ctx.Request.Header.Set("Signature", signature)
ctx.Request.Header.Set("Date", dateHeader)
ctx.Request.Header.Set("Digest", digestHeader)
ctx.Request.Header.Set("Content-Type", "application/activity+json")
// we need to pass the context through signature check first to set appropriate values on it
suite.signatureCheck(ctx)
// normally the router would populate these params from the path values,
// but because we're calling the function directly, we need to set them manually.
ctx.Params = gin.Params{
gin.Param{
Key: users.UsernameKey,
Value: blockedAccount.Username,
},
}
// trigger the function being tested
userModule.InboxPOSTHandler(ctx)
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
suite.NoError(err)
suite.Empty(b)
block := suite.newBlock(activityID, requestingAccount, targetAccount)
suite.inboxPost(block, requestingAccount, targetAccount, http.StatusAccepted, "")
// there should be a block in the database now between the accounts
dbBlock, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID)
suite.NoError(err)
suite.NotNil(dbBlock)
var (
dbBlock *gtsmodel.Block
err error
)
if !testrig.WaitFor(func() bool {
dbBlock, err = suite.db.GetBlock(context.Background(), requestingAccount.ID, targetAccount.ID)
return err == nil && dbBlock != nil
}) {
suite.FailNow("timed out waiting for block to be created")
}
suite.WithinDuration(time.Now(), dbBlock.CreatedAt, 30*time.Second)
suite.WithinDuration(time.Now(), dbBlock.UpdatedAt, 30*time.Second)
suite.Equal("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3", dbBlock.URI)

View file

@ -20,6 +20,7 @@
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -152,7 +153,7 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
activity, ok := t.(pub.Activity)
if !ok {
err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t)
err = fmt.Errorf("PostInboxScheme: ActivityStreams value with type %T is not a pub.Activity", t)
return true, err
}
@ -199,20 +200,22 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr
// target properties needed to be populated, but weren't.
//
// Send the rejection to the peer.
if err == pub.ErrObjectRequired || err == pub.ErrTargetRequired {
if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) {
l.Debugf("malformed incoming Activity: %s", err)
w.WriteHeader(http.StatusBadRequest)
return true, nil
}
// There's been some real error.
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err)
return true, err
}
// Our side effects are complete, now delegate determining whether to do inbox forwarding, as well as the action to do it.
if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {
err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err)
return true, err
}
// // Our side effects are complete, now delegate determining whether to do inbox forwarding, as well as the action to do it.
// if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil {
// err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err)
// return true, err
// }
// Request is now undergoing processing.
// Respond with an Accepted status.

View file

@ -0,0 +1,41 @@
package federatingdb
import (
"context"
"fmt"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
func (f *federatingDB) Block(ctx context.Context, block vocab.ActivityStreamsBlock) error {
receivingAccount, _ := extractFromCtx(ctx)
if receivingAccount == nil {
// If the receiving account wasn't set on the context, that means this request didn't pass
// through the API, but came from inside GtS as the result of another activity on this instance. That being so,
// we can safely just ignore this activity, since we know we've already processed it elsewhere.
return nil
}
gtsBlock, err := f.typeConverter.ASBlockToBlock(ctx, block)
if err != nil {
return fmt.Errorf("Block: could not convert Block to gts model block")
}
gtsBlock.ID = id.NewULID()
if err := f.state.DB.PutBlock(ctx, gtsBlock); err != nil {
return fmt.Errorf("Block: database error inserting block: %s", err)
}
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityCreate,
GTSModel: block,
ReceivingAccount: receivingAccount,
})
return nil
}

View file

@ -21,13 +21,13 @@
"context"
"errors"
"fmt"
"strings"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -66,9 +66,6 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
}
switch asType.GetTypeName() {
case ap.ActivityBlock:
// BLOCK SOMETHING
return f.activityBlock(ctx, asType, receivingAccount, requestingAccount)
case ap.ActivityCreate:
// CREATE SOMETHING
return f.activityCreate(ctx, asType, receivingAccount, requestingAccount)
@ -122,41 +119,52 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
func (f *federatingDB) activityCreate(ctx context.Context, asType vocab.Type, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
create, ok := asType.(vocab.ActivityStreamsCreate)
if !ok {
return errors.New("activityCreate: could not convert type to create")
return errors.New("activityCreate: could resolve %T to Create")
}
// create should have an object
// Create should have an object.
object := create.GetActivityStreamsObject()
if object == nil {
return errors.New("Create had no Object")
}
errs := []string{}
// iterate through the object(s) to see what we're meant to be creating
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
asObjectType := objectIter.GetType()
if asObjectType == nil {
// currently we can't do anything with just a Create of something that's not an Object with a type
// TODO: process a Create with an Object that's just a URI or something
errs = append(errs, "object of Create was not a Type")
// Iterate through the Object(s) to see what we're meant to be creating.
errs := make(gtserror.MultiError, 0, object.Len())
for iter := object.Begin(); iter != object.End(); iter = iter.Next() {
objectType := iter.GetType()
if objectType == nil {
// Currently we can't do anything with just a Create
// of something that's not an Object with a type.
errs.Append(errors.New("object of Create was not a Type"))
continue
}
// we have a type -- what is it?
asObjectTypeName := asObjectType.GetTypeName()
switch asObjectTypeName {
// Process object according to its type.
// TODO: possibly add more types here.
switch typeName := objectType.GetTypeName(); typeName {
case ap.ObjectNote:
// CREATE A NOTE
if err := f.createNote(ctx, objectIter.GetActivityStreamsNote(), receivingAccount, requestingAccount); err != nil {
errs = append(errs, err.Error())
if err := f.createNote(
ctx,
iter.GetActivityStreamsNote(),
receivingAccount,
requestingAccount,
); err != nil {
errs.Append(err)
}
default:
errs = append(errs, fmt.Sprintf("received an object on a Create that we couldn't handle: %s", asObjectType.GetTypeName()))
log.WithContext(ctx).
WithFields(kv.Fields{
{"receivingAccount", receivingAccount.URI},
{"requestingAccount", requestingAccount.URI},
{"typeName", typeName},
}...).
Debug("Object of Create was a type we couldn't handle")
}
}
if len(errs) != 0 {
return fmt.Errorf("activityCreate: one or more errors while processing activity: %s", strings.Join(errs, "; "))
return fmt.Errorf("activityCreate: one or more errors while processing activity: %w", errs.Combine())
}
return nil
@ -164,43 +172,44 @@ func (f *federatingDB) activityCreate(ctx context.Context, asType vocab.Type, re
// createNote handles a Create activity with a Note type.
func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStreamsNote, receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
l := log.WithContext(ctx).
WithFields(kv.Fields{
{"receivingAccount", receivingAccount.URI},
{"requestingAccount", requestingAccount.URI},
}...)
// Check if we have a forward.
// In other words, was the note posted to our inbox by at least one actor who actually created the note, or are they just forwarding it?
forward := true
// note should have an attributedTo
// Note must have an attributedTo for us to process it.
noteAttributedTo := note.GetActivityStreamsAttributedTo()
if noteAttributedTo == nil {
return errors.New("createNote: note had no attributedTo")
}
// compare the attributedTo(s) with the actor who posted this to our inbox
for attributedToIter := noteAttributedTo.Begin(); attributedToIter != noteAttributedTo.End(); attributedToIter = attributedToIter.Next() {
if !attributedToIter.IsIRI() {
// Check if we have a forward. In other words, was the
// note posted to our inbox by at least one actor who
// created the note, or are they just forwarding it?
forward := true
// Compare the attributedTo(s) with the URI of the Actor
// who posted this to our inbox. If the actor who posted
// the Note to our inbox is the same as at least one of
// the creators of the Note, then it's not a forward.
for iter := noteAttributedTo.Begin(); iter != noteAttributedTo.End(); iter = iter.Next() {
if !iter.IsIRI() {
continue
}
iri := attributedToIter.GetIRI()
if requestingAccount.URI == iri.String() {
// at least one creator of the note, and the actor who posted the note to our inbox, are the same, so it's not a forward
if iri := iter.GetIRI(); iri != nil && iri.String() == requestingAccount.URI {
forward = false
break
}
}
// If we do have a forward, we should ignore the content for now and just dereference based on the URL/ID of the note instead, to get the note straight from the horse's mouth
// If we do have a forward, we should ignore the content for
// now and just dereference based on the URL/ID of the note
// instead, to get the content straight from the poster's mouth.
if forward {
l.Trace("note is a forward")
id := note.GetJSONLDId()
if !id.IsIRI() {
// if the note id isn't an IRI, there's nothing we can do here
// If the ID isn't an IRI, then firstly this
// is weird, and secondly we can't process it.
return nil
}
// pass the note iri into the processor and have it do the dereferencing instead of doing it here
// Process the Note asynchronously, we're done here.
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
@ -209,33 +218,39 @@ func (f *federatingDB) createNote(ctx context.Context, note vocab.ActivityStream
GTSModel: nil,
ReceivingAccount: receivingAccount,
})
return nil
}
// if we reach this point, we know it's not a forwarded status, so proceed with processing it as normal
// If we reach this point, we know the status wasn't forwarded
// to us, but was delivered by at least one of the Actors who
// created it, so proceed with processing it as normal.
status, err := f.typeConverter.ASStatusToStatus(ctx, note)
if err != nil {
return fmt.Errorf("createNote: error converting note to status: %s", err)
return fmt.Errorf("createNote: error converting note to status: %w", err)
}
// id the status based on the time it was created
// id the status based on the time it was created;
// this allows for backdating of statuses.
statusID, err := id.NewULIDFromTime(status.CreatedAt)
if err != nil {
return err
return fmt.Errorf("createNote: error creating id for note: %w", err)
}
status.ID = statusID
if err := f.state.DB.PutStatus(ctx, status); err != nil {
if errors.Is(err, db.ErrAlreadyExists) {
// the status already exists in the database, which means we've already handled everything else,
// so we can just return nil here and be done with it.
// The status already exists in the database, which
// means we've already handled everything else, so
// we can just return nil here and be done with it.
return nil
}
// an actual error has happened
return fmt.Errorf("createNote: database error inserting status: %s", err)
// An actual error has happened.
return fmt.Errorf("createNote: database error inserting status: %w", err)
}
// Do further processing asynchronously.
f.state.Workers.EnqueueFederator(ctx, messages.FromFederator{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,

View file

@ -34,6 +34,7 @@ type DB interface {
Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error
Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error
Block(ctx context.Context, block vocab.ActivityStreamsBlock) error
}
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.

View file

@ -0,0 +1,3 @@
package federatingdb

View file

@ -378,6 +378,9 @@ func(ctx context.Context, reject vocab.ActivityStreamsReject) error {
func(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error {
return f.FederatingDB().Announce(ctx, announce)
},
func(ctx context.Context, block vocab.ActivityStreamsBlock) error {
return f.FederatingDB().Block(ctx, block)
},
}
return

View file

@ -21,7 +21,6 @@
"context"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/superseriousbusiness/gotosocial/internal/ap"
@ -29,20 +28,6 @@
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
//
// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox.
// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
//
// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
//
// If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
//
// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
func (p *Processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return p.federator.FederatingActor().PostInbox(ctx, w, r)
}
// OutboxGet returns the activitypub representation of a local user's outbox.
// This contains links to PUBLIC posts made by this user.
func (p *Processor) OutboxGet(ctx context.Context, requestedUsername string, page bool, maxID string, minID string) (interface{}, gtserror.WithCode) {

View file

@ -0,0 +1,37 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package fedi
import (
"context"
"net/http"
)
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
//
// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox.
// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page.
//
// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written.
//
// If the Actor was constructed with the Federated Protocol enabled, side effects will occur.
//
// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur.
func (p *Processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
return p.federator.FederatingActor().PostInbox(ctx, w, r)
}