mirror of
https://github.com/go-gitea/gitea
synced 2024-12-26 10:34:26 +00:00
activitypub: implement the ReqSignature middleware
Signed-off-by: Loïc Dachary <loic@dachary.org>
This commit is contained in:
parent
15c1f6218c
commit
97fedf2616
@ -9,9 +9,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams"
|
||||
@ -32,7 +35,7 @@ func TestActivityPubPerson(t *testing.T) {
|
||||
username := "user2"
|
||||
req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
assert.Contains(t, string(resp.Body.Bytes()), "@context")
|
||||
assert.Contains(t, resp.Body.String(), "@context")
|
||||
var m map[string]interface{}
|
||||
_ = json.Unmarshal(resp.Body.Bytes(), &m)
|
||||
|
||||
@ -46,26 +49,26 @@ func TestActivityPubPerson(t *testing.T) {
|
||||
assert.Equal(t, err, nil)
|
||||
assert.Equal(t, "Person", person.GetTypeName())
|
||||
assert.Equal(t, username, person.GetActivityStreamsName().Begin().GetXMLSchemaString())
|
||||
keyId := person.GetJSONLDId().GetIRI().String()
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyId)
|
||||
keyID := person.GetJSONLDId().GetIRI().String()
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID)
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.GetActivityStreamsOutbox().GetIRI().String())
|
||||
assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.GetActivityStreamsInbox().GetIRI().String())
|
||||
|
||||
pkp := person.GetW3IDSecurityV1PublicKey()
|
||||
publicKeyId := keyId + "/#main-key"
|
||||
publicKeyID := keyID + "/#main-key"
|
||||
var pkpFound vocab.W3IDSecurityV1PublicKey
|
||||
for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
|
||||
if !pkpIter.IsW3IDSecurityV1PublicKey() {
|
||||
continue
|
||||
}
|
||||
pkValue := pkpIter.Get()
|
||||
var pkId *url.URL
|
||||
pkId, err = pub.GetId(pkValue)
|
||||
var pkID *url.URL
|
||||
pkID, err = pub.GetId(pkValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, pkId.String(), publicKeyId)
|
||||
if pkId.String() != publicKeyId {
|
||||
assert.Equal(t, pkID.String(), publicKeyID)
|
||||
if pkID.String() != publicKeyID {
|
||||
continue
|
||||
}
|
||||
pkpFound = pkValue
|
||||
@ -91,6 +94,40 @@ func TestActivityPubMissingPerson(t *testing.T) {
|
||||
|
||||
req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser")
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
assert.Contains(t, string(resp.Body.Bytes()), "GetUserByName")
|
||||
assert.Contains(t, resp.Body.String(), "GetUserByName")
|
||||
})
|
||||
}
|
||||
|
||||
func TestActivityPubPersonInbox(t *testing.T) {
|
||||
srv := httptest.NewServer(c)
|
||||
defer srv.Close()
|
||||
|
||||
onGiteaRun(t, func(*testing.T, *url.URL) {
|
||||
appURL := setting.AppURL
|
||||
setting.Federation.Enabled = true
|
||||
setting.Database.LogSQL = true
|
||||
setting.AppURL = srv.URL
|
||||
defer func() {
|
||||
setting.Federation.Enabled = false
|
||||
setting.Database.LogSQL = false
|
||||
setting.AppURL = appURL
|
||||
}()
|
||||
username1 := "user1"
|
||||
user1, err := user_model.GetUserByName(username1)
|
||||
assert.NoError(t, err)
|
||||
user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s/#main-key", srv.URL, username1)
|
||||
c, err := activitypub.NewClient(user1, user1url)
|
||||
assert.NoError(t, err)
|
||||
username2 := "user2"
|
||||
user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2)
|
||||
|
||||
// Signed request succeeds
|
||||
resp, err := c.Post([]byte{}, user2inboxurl)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 204, resp.StatusCode)
|
||||
|
||||
// Unsigned request fails
|
||||
req := NewRequest(t, "POST", user2inboxurl)
|
||||
MakeRequest(t, req, 500)
|
||||
})
|
||||
}
|
||||
|
@ -19,10 +19,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
activityStreamsContentType = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
// ActivityStreamsContentType const
|
||||
ActivityStreamsContentType = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
)
|
||||
|
||||
func containsRequiredHttpHeaders(method string, headers []string) error {
|
||||
func containsRequiredHTTPHeaders(method string, headers []string) error {
|
||||
var hasRequestTarget, hasDate, hasDigest bool
|
||||
for _, header := range headers {
|
||||
hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
|
||||
@ -39,6 +40,7 @@ func containsRequiredHttpHeaders(method string, headers []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Client struct
|
||||
type Client struct {
|
||||
clock pub.Clock
|
||||
client *http.Client
|
||||
@ -47,13 +49,14 @@ type Client struct {
|
||||
getHeaders []string
|
||||
postHeaders []string
|
||||
priv *rsa.PrivateKey
|
||||
pubId string
|
||||
pubID string
|
||||
}
|
||||
|
||||
func NewClient(user *user_model.User, pubId string) (c *Client, err error) {
|
||||
if err = containsRequiredHttpHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
|
||||
// NewClient function
|
||||
func NewClient(user *user_model.User, pubID string) (c *Client, err error) {
|
||||
if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
|
||||
return
|
||||
} else if err = containsRequiredHttpHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
|
||||
} else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
|
||||
return
|
||||
} else if !httpsig.IsSupportedDigestAlgorithm(setting.Federation.DigestAlgorithm) {
|
||||
err = fmt.Errorf("unsupported digest algorithm: %s", setting.Federation.DigestAlgorithm)
|
||||
@ -86,21 +89,21 @@ func NewClient(user *user_model.User, pubId string) (c *Client, err error) {
|
||||
getHeaders: setting.Federation.GetHeaders,
|
||||
postHeaders: setting.Federation.PostHeaders,
|
||||
priv: privParsed,
|
||||
pubId: pubId,
|
||||
pubID: pubID,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
||||
// NewRequest function
|
||||
func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) {
|
||||
byteCopy := make([]byte, len(b))
|
||||
copy(byteCopy, b)
|
||||
buf := bytes.NewBuffer(byteCopy)
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest(http.MethodPost, to, buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Add("Content-Type", activityStreamsContentType)
|
||||
req.Header.Add("Content-Type", ActivityStreamsContentType)
|
||||
req.Header.Add("Accept-Charset", "utf-8")
|
||||
req.Header.Add("Date", fmt.Sprintf("%s GMT", c.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")))
|
||||
|
||||
@ -108,8 +111,14 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = signer.SignRequest(c.priv, c.pubId, req, b)
|
||||
if err != nil {
|
||||
err = signer.SignRequest(c.priv, c.pubID, req, b)
|
||||
return
|
||||
}
|
||||
|
||||
// Post function
|
||||
func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
||||
var req *http.Request
|
||||
if req, err = c.NewRequest(b, to); err != nil {
|
||||
return
|
||||
}
|
||||
resp, err = c.client.Do(req)
|
||||
|
@ -7,7 +7,6 @@ package activitypub
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
@ -24,16 +23,16 @@ import (
|
||||
func TestActivityPubSignedPost(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
pubId := "https://example.com/pubId"
|
||||
c, err := NewClient(user, pubId)
|
||||
pubID := "https://example.com/pubID"
|
||||
c, err := NewClient(user, pubID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := "BODY"
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest"))
|
||||
assert.Contains(t, r.Header.Get("Signature"), pubId)
|
||||
assert.Equal(t, r.Header.Get("Content-Type"), activityStreamsContentType)
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
assert.Contains(t, r.Header.Get("Signature"), pubID)
|
||||
assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(body))
|
||||
fmt.Fprintf(w, expected)
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
package structs
|
||||
|
||||
// ActivityPub type
|
||||
type ActivityPub struct {
|
||||
Context string `json:"@context"`
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -17,32 +16,9 @@ import (
|
||||
"github.com/go-fed/activity/streams"
|
||||
)
|
||||
|
||||
// hack waiting on https://github.com/go-gitea/gitea/pull/16834
|
||||
func GetPublicKey(user *models.User) (string, error) {
|
||||
if settings, err := models.GetUserSetting(user.ID, []string{"activitypub_pubPem"}); err != nil {
|
||||
return "", err
|
||||
} else if len(settings) == 0 {
|
||||
if priv, pub, err := activitypub.GenerateKeyPair(); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
privPem := &models.UserSetting{UserID: user.ID, Name: "activitypub_privPem", Value: priv}
|
||||
if err := models.SetUserSetting(privPem); err != nil {
|
||||
return "", err
|
||||
}
|
||||
pubPem := &models.UserSetting{UserID: user.ID, Name: "activitypub_pubPem", Value: pub}
|
||||
if err := models.SetUserSetting(pubPem); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return pubPem.Value, nil
|
||||
}
|
||||
} else {
|
||||
return settings[0].Value, nil
|
||||
}
|
||||
}
|
||||
|
||||
// NodeInfo returns the NodeInfo for the Gitea instance to allow for federation
|
||||
// Person function
|
||||
func Person(ctx *context.APIContext) {
|
||||
// swagger:operation GET /activitypub/user/{username} information
|
||||
// swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson
|
||||
// ---
|
||||
// summary: Returns the person
|
||||
// produces:
|
||||
@ -73,30 +49,30 @@ func Person(ctx *context.APIContext) {
|
||||
person.SetActivityStreamsName(name)
|
||||
|
||||
ibox := streams.NewActivityStreamsInboxProperty()
|
||||
url_object, _ := url.Parse(link + "/inbox")
|
||||
ibox.SetIRI(url_object)
|
||||
urlObject, _ := url.Parse(link + "/inbox")
|
||||
ibox.SetIRI(urlObject)
|
||||
person.SetActivityStreamsInbox(ibox)
|
||||
|
||||
obox := streams.NewActivityStreamsOutboxProperty()
|
||||
url_object, _ = url.Parse(link + "/outbox")
|
||||
obox.SetIRI(url_object)
|
||||
urlObject, _ = url.Parse(link + "/outbox")
|
||||
obox.SetIRI(urlObject)
|
||||
person.SetActivityStreamsOutbox(obox)
|
||||
|
||||
publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
|
||||
|
||||
publicKeyType := streams.NewW3IDSecurityV1PublicKey()
|
||||
|
||||
pubKeyIdProp := streams.NewJSONLDIdProperty()
|
||||
pubKeyIDProp := streams.NewJSONLDIdProperty()
|
||||
pubKeyIRI, _ := url.Parse(link + "/#main-key")
|
||||
pubKeyIdProp.SetIRI(pubKeyIRI)
|
||||
publicKeyType.SetJSONLDId(pubKeyIdProp)
|
||||
pubKeyIDProp.SetIRI(pubKeyIRI)
|
||||
publicKeyType.SetJSONLDId(pubKeyIDProp)
|
||||
|
||||
ownerProp := streams.NewW3IDSecurityV1OwnerProperty()
|
||||
ownerProp.SetIRI(idIRI)
|
||||
publicKeyType.SetW3IDSecurityV1Owner(ownerProp)
|
||||
|
||||
publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
|
||||
if publicKeyPem, err := GetPublicKey(user); err != nil {
|
||||
if publicKeyPem, err := activitypub.GetPublicKey(user); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetPublicKey", err)
|
||||
} else {
|
||||
publicKeyPemProp.Set(publicKeyPem)
|
||||
@ -110,3 +86,24 @@ func Person(ctx *context.APIContext) {
|
||||
jsonmap, _ = streams.Serialize(person)
|
||||
ctx.JSON(http.StatusOK, jsonmap)
|
||||
}
|
||||
|
||||
// PersonInbox function
|
||||
func PersonInbox(ctx *context.APIContext) {
|
||||
// swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox
|
||||
// ---
|
||||
// summary: Send to the inbox
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of the user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
158
routers/api/v1/activitypub/reqsignature.go
Normal file
158
routers/api/v1/activitypub/reqsignature.go
Normal file
@ -0,0 +1,158 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package activitypub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
gitea_context "code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/go-fed/activity/pub"
|
||||
"github.com/go-fed/activity/streams"
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/go-fed/httpsig"
|
||||
)
|
||||
|
||||
type publicKeyer interface {
|
||||
GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty
|
||||
}
|
||||
|
||||
func getPublicKeyFromResponse(ctx context.Context, b []byte, keyID *url.URL) (p crypto.PublicKey, err error) {
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var t vocab.Type
|
||||
t, err = streams.ToType(ctx, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pker, ok := t.(publicKeyer)
|
||||
if !ok {
|
||||
err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t)
|
||||
return
|
||||
}
|
||||
pkp := pker.GetW3IDSecurityV1PublicKey()
|
||||
if pkp == nil {
|
||||
err = fmt.Errorf("publicKey property is not provided")
|
||||
return
|
||||
}
|
||||
var pkpFound vocab.W3IDSecurityV1PublicKey
|
||||
for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
|
||||
if !pkpIter.IsW3IDSecurityV1PublicKey() {
|
||||
continue
|
||||
}
|
||||
pkValue := pkpIter.Get()
|
||||
var pkID *url.URL
|
||||
pkID, err = pub.GetId(pkValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if pkID.String() != keyID.String() {
|
||||
continue
|
||||
}
|
||||
pkpFound = pkValue
|
||||
break
|
||||
}
|
||||
if pkpFound == nil {
|
||||
err = fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, b)
|
||||
return
|
||||
}
|
||||
pkPemProp := pkpFound.GetW3IDSecurityV1PublicKeyPem()
|
||||
if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
|
||||
err = fmt.Errorf("publicKeyPem property is not provided or it is not embedded as a value")
|
||||
return
|
||||
}
|
||||
pubKeyPem := pkPemProp.Get()
|
||||
var block *pem.Block
|
||||
block, _ = pem.Decode([]byte(pubKeyPem))
|
||||
if block == nil || block.Type != "PUBLIC KEY" {
|
||||
err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type")
|
||||
return
|
||||
}
|
||||
p, err = x509.ParsePKIXPublicKey(block.Bytes)
|
||||
return
|
||||
}
|
||||
|
||||
func fetch(iri *url.URL) (b []byte, err error) {
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest(http.MethodGet, iri.String(), nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Add("Accept", activitypub.ActivityStreamsContentType)
|
||||
req.Header.Add("Accept-Charset", "utf-8")
|
||||
clock, err := activitypub.NewClock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req.Header.Add("Date", fmt.Sprintf("%s GMT", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")))
|
||||
var resp *http.Response
|
||||
client := &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status)
|
||||
return
|
||||
}
|
||||
b, err = io.ReadAll(resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) {
|
||||
r := ctx.Req
|
||||
|
||||
// 1. Figure out what key we need to verify
|
||||
var v httpsig.Verifier
|
||||
v, err = httpsig.NewVerifier(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ID := v.KeyId()
|
||||
var idIRI *url.URL
|
||||
idIRI, err = url.Parse(ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 2. Fetch the public key of the other actor
|
||||
var b []byte
|
||||
b, err = fetch(idIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pKey, err := getPublicKeyFromResponse(*ctx, b, idIRI)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// 3. Verify the other actor's key
|
||||
algo := httpsig.Algorithm(setting.Federation.Algorithms[0])
|
||||
authenticated = nil == v.Verify(pKey, algo)
|
||||
return
|
||||
}
|
||||
|
||||
// ReqSignature function
|
||||
func ReqSignature() func(ctx *gitea_context.APIContext) {
|
||||
return func(ctx *gitea_context.APIContext) {
|
||||
if authenticated, err := verifyHTTPSignatures(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "verifyHttpSignatures", err)
|
||||
} else if !authenticated {
|
||||
ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed")
|
||||
}
|
||||
}
|
||||
}
|
@ -602,6 +602,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
|
||||
m.Group("/user/{username}", func() {
|
||||
m.Get("", activitypub.Person)
|
||||
})
|
||||
m.Post("/user/{username}/inbox", activitypub.ReqSignature(), activitypub.PersonInbox)
|
||||
})
|
||||
}
|
||||
m.Get("/signing-key.gpg", misc.SigningKey)
|
||||
|
@ -28,8 +28,11 @@
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activitypub"
|
||||
],
|
||||
"summary": "Returns the person",
|
||||
"operationId": "information",
|
||||
"operationId": "activitypubPerson",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@ -46,6 +49,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/activitypub/user/{username}/inbox": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"activitypub"
|
||||
],
|
||||
"summary": "Send to the inbox",
|
||||
"operationId": "activitypubPersonInbox",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "username of the user",
|
||||
"name": "username",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"$ref": "#/responses/empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/admin/cron": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
Loading…
Reference in New Issue
Block a user