mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-10 07:47:29 -06:00
[chore] The Big Middleware and API Refactor (tm) (#1250)
* interim commit: start refactoring middlewares into package under router * another interim commit, this is becoming a big job * another fucking massive interim commit * refactor bookmarks to new style * ambassador, wiz zeze commits you are spoiling uz * she compiles, we're getting there * we're just normal men; we're just innocent men * apiutil * whoopsie * i'm glad noone reads commit msgs haha :blob_sweat: * use that weirdo go-bytesize library for maxMultipartMemory * fix media module paths
This commit is contained in:
parent
560ff1209d
commit
941893a774
228 changed files with 3188 additions and 3047 deletions
47
internal/api/activitypub/emoji/emoji.go
Normal file
47
internal/api/activitypub/emoji/emoji.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 emoji
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
)
|
||||
|
||||
const (
|
||||
// EmojiIDKey is for emoji IDs
|
||||
EmojiIDKey = "id"
|
||||
// EmojiBasePath is the base path for serving AP Emojis, minus the "emoji" prefix
|
||||
EmojiWithIDPath = "/:" + EmojiIDKey
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
processor processing.Processor
|
||||
}
|
||||
|
||||
func New(processor processing.Processor) *Module {
|
||||
return &Module{
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, EmojiWithIDPath, m.EmojiGetHandler)
|
||||
}
|
||||
59
internal/api/activitypub/emoji/emojiget.go
Normal file
59
internal/api/activitypub/emoji/emojiget.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 emoji
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
func (m *Module) EmojiGetHandler(c *gin.Context) {
|
||||
requestedEmojiID := strings.ToUpper(c.Param(EmojiIDKey))
|
||||
if requestedEmojiID == "" {
|
||||
err := errors.New("no emoji id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.ActivityPubAcceptHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediEmoji(apiutil.TransferSignatureContext(c), requestedEmojiID, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
137
internal/api/activitypub/emoji/emojiget_test.go
Normal file
137
internal/api/activitypub/emoji/emojiget_test.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 emoji_test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type EmojiGetTestSuite struct {
|
||||
suite.Suite
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
mediaManager media.Manager
|
||||
federator federation.Federator
|
||||
emailSender email.Sender
|
||||
processor processing.Processor
|
||||
storage *storage.Driver
|
||||
|
||||
testEmojis map[string]*gtsmodel.Emoji
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
|
||||
emojiModule *emoji.Module
|
||||
|
||||
signatureCheck gin.HandlerFunc
|
||||
}
|
||||
|
||||
func (suite *EmojiGetTestSuite) SetupSuite() {
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testEmojis = testrig.NewTestEmojis()
|
||||
}
|
||||
|
||||
func (suite *EmojiGetTestSuite) SetupTest() {
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
suite.emojiModule = emoji.New(suite.processor)
|
||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||
|
||||
suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked)
|
||||
|
||||
suite.NoError(suite.processor.Start())
|
||||
}
|
||||
|
||||
func (suite *EmojiGetTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
}
|
||||
|
||||
func (suite *EmojiGetTestSuite) TestGetEmoji() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_emoji"]
|
||||
targetEmoji := suite.testEmojis["rainbow"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetEmoji.URI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: emoji.EmojiIDKey,
|
||||
Value: targetEmoji.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.emojiModule.EmojiGetHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Contains(string(b), `"icon":{"mediaType":"image/png","type":"Image","url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"},"id":"http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ","name":":rainbow:","type":"Emoji"`)
|
||||
}
|
||||
|
||||
func TestEmojiGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(EmojiGetTestSuite))
|
||||
}
|
||||
57
internal/api/activitypub/users/common.go
Normal file
57
internal/api/activitypub/users/common.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
// SwaggerCollection represents an activitypub collection.
|
||||
// swagger:model swaggerCollection
|
||||
type SwaggerCollection struct {
|
||||
// ActivityStreams context.
|
||||
// example: https://www.w3.org/ns/activitystreams
|
||||
Context string `json:"@context"`
|
||||
// ActivityStreams ID.
|
||||
// example: https://example.org/users/some_user/statuses/106717595988259568/replies
|
||||
ID string `json:"id"`
|
||||
// ActivityStreams type.
|
||||
// example: Collection
|
||||
Type string `json:"type"`
|
||||
// ActivityStreams first property.
|
||||
First SwaggerCollectionPage `json:"first"`
|
||||
// ActivityStreams last property.
|
||||
Last SwaggerCollectionPage `json:"last,omitempty"`
|
||||
}
|
||||
|
||||
// SwaggerCollectionPage represents one page of a collection.
|
||||
// swagger:model swaggerCollectionPage
|
||||
type SwaggerCollectionPage struct {
|
||||
// ActivityStreams ID.
|
||||
// example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true
|
||||
ID string `json:"id"`
|
||||
// ActivityStreams type.
|
||||
// example: CollectionPage
|
||||
Type string `json:"type"`
|
||||
// Link to the next page.
|
||||
// example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true
|
||||
Next string `json:"next"`
|
||||
// Collection this page belongs to.
|
||||
// example: https://example.org/users/some_user/statuses/106717595988259568/replies
|
||||
PartOf string `json:"partOf"`
|
||||
// Items on this page.
|
||||
// example: ["https://example.org/users/some_other_user/statuses/086417595981111564", "https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R"]
|
||||
Items []string `json:"items"`
|
||||
}
|
||||
67
internal/api/activitypub/users/followers.go
Normal file
67
internal/api/activitypub/users/followers.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowersGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediFollowers(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
67
internal/api/activitypub/users/following.go
Normal file
67
internal/api/activitypub/users/following.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
||||
func (m *Module) FollowingGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediFollowing(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
51
internal/api/activitypub/users/inboxpost.go
Normal file
51
internal/api/activitypub/users/inboxpost.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
|
||||
)
|
||||
|
||||
// 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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if posted, err := m.processor.InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil {
|
||||
if withCode, ok := err.(gtserror.WithCode); ok {
|
||||
apiutil.ErrorHandler(c, withCode, m.processor.InstanceGet)
|
||||
} else {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
}
|
||||
} else if !posted {
|
||||
err := errors.New("unable to process request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
}
|
||||
}
|
||||
500
internal/api/activitypub/users/inboxpost_test.go
Normal file
500
internal/api/activitypub/users/inboxpost_test.go
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
block := streams.NewActivityStreamsBlock()
|
||||
|
||||
// set the actor property to the block-ing account's URI
|
||||
actorProp := streams.NewActivityStreamsActorProperty()
|
||||
actorIRI := testrig.URLMustParse(blockingAccount.URI)
|
||||
actorProp.AppendIRI(actorIRI)
|
||||
block.SetActivityStreamsActor(actorProp)
|
||||
|
||||
// set the ID property to the blocks's URI
|
||||
idProp := streams.NewJSONLDIdProperty()
|
||||
idProp.Set(blockURI)
|
||||
block.SetJSONLDId(idProp)
|
||||
|
||||
// set the object property to the target account's URI
|
||||
objectProp := streams.NewActivityStreamsObjectProperty()
|
||||
targetIRI := testrig.URLMustParse(blockedAccount.URI)
|
||||
objectProp.AppendIRI(targetIRI)
|
||||
block.SetActivityStreamsObject(objectProp)
|
||||
|
||||
// set the TO property to the target account's IRI
|
||||
toProp := streams.NewActivityStreamsToProperty()
|
||||
toIRI := testrig.URLMustParse(blockedAccount.URI)
|
||||
toProp.AppendIRI(toIRI)
|
||||
block.SetActivityStreamsTo(toProp)
|
||||
|
||||
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
|
||||
|
||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
|
||||
bodyI, err := streams.Serialize(block)
|
||||
suite.NoError(err)
|
||||
|
||||
bodyJson, err := json.Marshal(bodyI)
|
||||
suite.NoError(err)
|
||||
body := bytes.NewReader(bodyJson)
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
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)
|
||||
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
||||
// TestPostUnblock verifies that a remote account with a block targeting one of our instance users should be able to undo that block.
|
||||
func (suite *InboxPostTestSuite) TestPostUnblock() {
|
||||
blockingAccount := suite.testAccounts["remote_account_1"]
|
||||
blockedAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// first put a block in the database so we have something to undo
|
||||
blockURI := "http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3"
|
||||
dbBlockID, err := id.NewRandomULID()
|
||||
suite.NoError(err)
|
||||
|
||||
dbBlock := >smodel.Block{
|
||||
ID: dbBlockID,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
URI: blockURI,
|
||||
AccountID: blockingAccount.ID,
|
||||
TargetAccountID: blockedAccount.ID,
|
||||
}
|
||||
|
||||
err = suite.db.PutBlock(context.Background(), dbBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
asBlock, err := suite.tc.BlockToAS(context.Background(), dbBlock)
|
||||
suite.NoError(err)
|
||||
|
||||
targetAccountURI := testrig.URLMustParse(blockedAccount.URI)
|
||||
|
||||
// create an Undo and set the appropriate actor on it
|
||||
undo := streams.NewActivityStreamsUndo()
|
||||
undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor())
|
||||
|
||||
// Set the block as the 'object' property.
|
||||
undoObject := streams.NewActivityStreamsObjectProperty()
|
||||
undoObject.AppendActivityStreamsBlock(asBlock)
|
||||
undo.SetActivityStreamsObject(undoObject)
|
||||
|
||||
// Set the To of the undo as the target of the block
|
||||
undoTo := streams.NewActivityStreamsToProperty()
|
||||
undoTo.AppendIRI(targetAccountURI)
|
||||
undo.SetActivityStreamsTo(undoTo)
|
||||
|
||||
undoID := streams.NewJSONLDIdProperty()
|
||||
undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5"))
|
||||
undo.SetJSONLDId(undoID)
|
||||
|
||||
targetURI := testrig.URLMustParse(blockedAccount.InboxURI)
|
||||
|
||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(undo, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI)
|
||||
bodyI, err := streams.Serialize(undo)
|
||||
suite.NoError(err)
|
||||
|
||||
bodyJson, err := json.Marshal(bodyI)
|
||||
suite.NoError(err)
|
||||
body := bytes.NewReader(bodyJson)
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
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)
|
||||
suite.Equal(http.StatusOK, result.StatusCode)
|
||||
|
||||
// the block should be undone
|
||||
block, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(block)
|
||||
}
|
||||
|
||||
func (suite *InboxPostTestSuite) TestPostUpdate() {
|
||||
updatedAccount := *suite.testAccounts["remote_account_1"]
|
||||
updatedAccount.DisplayName = "updated display name!"
|
||||
|
||||
// ad an emoji to the account; because we're serializing this remote
|
||||
// account from our own instance, we need to cheat a bit to get the emoji
|
||||
// to work properly, just for this test
|
||||
testEmoji := >smodel.Emoji{}
|
||||
*testEmoji = *testrig.NewTestEmojis()["yell"]
|
||||
testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat
|
||||
updatedAccount.Emojis = []*gtsmodel.Emoji{testEmoji}
|
||||
|
||||
asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount)
|
||||
suite.NoError(err)
|
||||
|
||||
receivingAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// create an update
|
||||
update := streams.NewActivityStreamsUpdate()
|
||||
|
||||
// set the appropriate actor on it
|
||||
updateActor := streams.NewActivityStreamsActorProperty()
|
||||
updateActor.AppendIRI(testrig.URLMustParse(updatedAccount.URI))
|
||||
update.SetActivityStreamsActor(updateActor)
|
||||
|
||||
// Set the account as the 'object' property.
|
||||
updateObject := streams.NewActivityStreamsObjectProperty()
|
||||
updateObject.AppendActivityStreamsPerson(asAccount)
|
||||
update.SetActivityStreamsObject(updateObject)
|
||||
|
||||
// Set the To of the update as public
|
||||
updateTo := streams.NewActivityStreamsToProperty()
|
||||
updateTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI))
|
||||
update.SetActivityStreamsTo(updateTo)
|
||||
|
||||
// set the cc of the update to the receivingAccount
|
||||
updateCC := streams.NewActivityStreamsCcProperty()
|
||||
updateCC.AppendIRI(testrig.URLMustParse(receivingAccount.URI))
|
||||
update.SetActivityStreamsCc(updateCC)
|
||||
|
||||
// set some random-ass ID for the activity
|
||||
undoID := streams.NewJSONLDIdProperty()
|
||||
undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b"))
|
||||
update.SetJSONLDId(undoID)
|
||||
|
||||
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
|
||||
|
||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(update, updatedAccount.PublicKeyURI, updatedAccount.PrivateKey, targetURI)
|
||||
bodyI, err := streams.Serialize(update)
|
||||
suite.NoError(err)
|
||||
|
||||
bodyJson, err := json.Marshal(bodyI)
|
||||
suite.NoError(err)
|
||||
body := bytes.NewReader(bodyJson)
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
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: receivingAccount.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)
|
||||
suite.Equal(http.StatusOK, result.StatusCode)
|
||||
|
||||
// account should be changed in the database now
|
||||
var dbUpdatedAccount *gtsmodel.Account
|
||||
|
||||
if !testrig.WaitFor(func() bool {
|
||||
// displayName should be updated
|
||||
dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), updatedAccount.ID)
|
||||
return dbUpdatedAccount.DisplayName == "updated display name!"
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for account update")
|
||||
}
|
||||
|
||||
// emojis should be updated
|
||||
suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID)
|
||||
|
||||
// account should be freshly webfingered
|
||||
suite.WithinDuration(time.Now(), dbUpdatedAccount.LastWebfingeredAt, 10*time.Second)
|
||||
|
||||
// everything else should be the same as it was before
|
||||
suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username)
|
||||
suite.EqualValues(updatedAccount.Domain, dbUpdatedAccount.Domain)
|
||||
suite.EqualValues(updatedAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID)
|
||||
suite.EqualValues(updatedAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment)
|
||||
suite.EqualValues(updatedAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL)
|
||||
suite.EqualValues(updatedAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID)
|
||||
suite.EqualValues(updatedAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment)
|
||||
suite.EqualValues(updatedAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL)
|
||||
suite.EqualValues(updatedAccount.Note, dbUpdatedAccount.Note)
|
||||
suite.EqualValues(updatedAccount.Memorial, dbUpdatedAccount.Memorial)
|
||||
suite.EqualValues(updatedAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs)
|
||||
suite.EqualValues(updatedAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID)
|
||||
suite.EqualValues(updatedAccount.Bot, dbUpdatedAccount.Bot)
|
||||
suite.EqualValues(updatedAccount.Reason, dbUpdatedAccount.Reason)
|
||||
suite.EqualValues(updatedAccount.Locked, dbUpdatedAccount.Locked)
|
||||
suite.EqualValues(updatedAccount.Discoverable, dbUpdatedAccount.Discoverable)
|
||||
suite.EqualValues(updatedAccount.Privacy, dbUpdatedAccount.Privacy)
|
||||
suite.EqualValues(updatedAccount.Sensitive, dbUpdatedAccount.Sensitive)
|
||||
suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language)
|
||||
suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI)
|
||||
suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL)
|
||||
suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI)
|
||||
suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI)
|
||||
suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI)
|
||||
suite.EqualValues(updatedAccount.FollowersURI, dbUpdatedAccount.FollowersURI)
|
||||
suite.EqualValues(updatedAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI)
|
||||
suite.EqualValues(updatedAccount.ActorType, dbUpdatedAccount.ActorType)
|
||||
suite.EqualValues(updatedAccount.PublicKey, dbUpdatedAccount.PublicKey)
|
||||
suite.EqualValues(updatedAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI)
|
||||
suite.EqualValues(updatedAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt)
|
||||
suite.EqualValues(updatedAccount.SilencedAt, dbUpdatedAccount.SilencedAt)
|
||||
suite.EqualValues(updatedAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt)
|
||||
suite.EqualValues(updatedAccount.HideCollections, dbUpdatedAccount.HideCollections)
|
||||
suite.EqualValues(updatedAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin)
|
||||
}
|
||||
|
||||
func (suite *InboxPostTestSuite) TestPostDelete() {
|
||||
deletedAccount := *suite.testAccounts["remote_account_1"]
|
||||
receivingAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// create a delete
|
||||
delete := streams.NewActivityStreamsDelete()
|
||||
|
||||
// set the appropriate actor on it
|
||||
deleteActor := streams.NewActivityStreamsActorProperty()
|
||||
deleteActor.AppendIRI(testrig.URLMustParse(deletedAccount.URI))
|
||||
delete.SetActivityStreamsActor(deleteActor)
|
||||
|
||||
// Set the account iri as the 'object' property.
|
||||
deleteObject := streams.NewActivityStreamsObjectProperty()
|
||||
deleteObject.AppendIRI(testrig.URLMustParse(deletedAccount.URI))
|
||||
delete.SetActivityStreamsObject(deleteObject)
|
||||
|
||||
// Set the To of the delete as public
|
||||
deleteTo := streams.NewActivityStreamsToProperty()
|
||||
deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI))
|
||||
delete.SetActivityStreamsTo(deleteTo)
|
||||
|
||||
// set some random-ass ID for the activity
|
||||
deleteID := streams.NewJSONLDIdProperty()
|
||||
deleteID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b"))
|
||||
delete.SetJSONLDId(deleteID)
|
||||
|
||||
targetURI := testrig.URLMustParse(receivingAccount.InboxURI)
|
||||
|
||||
signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(delete, deletedAccount.PublicKeyURI, deletedAccount.PrivateKey, targetURI)
|
||||
bodyI, err := streams.Serialize(delete)
|
||||
suite.NoError(err)
|
||||
|
||||
bodyJson, err := json.Marshal(bodyI)
|
||||
suite.NoError(err)
|
||||
body := bytes.NewReader(bodyJson)
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
suite.NoError(processor.Start())
|
||||
userModule := users.New(processor)
|
||||
|
||||
// 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: receivingAccount.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)
|
||||
suite.Equal(http.StatusOK, result.StatusCode)
|
||||
|
||||
if !testrig.WaitFor(func() bool {
|
||||
// local account 2 blocked foss_satan, that block should be gone now
|
||||
testBlock := suite.testBlocks["local_account_2_block_remote_account_1"]
|
||||
dbBlock := >smodel.Block{}
|
||||
err = suite.db.GetByID(ctx, testBlock.ID, dbBlock)
|
||||
return suite.ErrorIs(err, db.ErrNoEntries)
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for block to be removed")
|
||||
}
|
||||
|
||||
// no statuses from foss satan should be left in the database
|
||||
dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false)
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Empty(dbStatuses)
|
||||
|
||||
dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID)
|
||||
suite.NoError(err)
|
||||
|
||||
suite.Empty(dbAccount.Note)
|
||||
suite.Empty(dbAccount.DisplayName)
|
||||
suite.Empty(dbAccount.AvatarMediaAttachmentID)
|
||||
suite.Empty(dbAccount.AvatarRemoteURL)
|
||||
suite.Empty(dbAccount.HeaderMediaAttachmentID)
|
||||
suite.Empty(dbAccount.HeaderRemoteURL)
|
||||
suite.Empty(dbAccount.Reason)
|
||||
suite.Empty(dbAccount.Fields)
|
||||
suite.True(*dbAccount.HideCollections)
|
||||
suite.False(*dbAccount.Discoverable)
|
||||
suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second)
|
||||
suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin)
|
||||
}
|
||||
|
||||
func TestInboxPostTestSuite(t *testing.T) {
|
||||
suite.Run(t, &InboxPostTestSuite{})
|
||||
}
|
||||
145
internal/api/activitypub/users/outboxget.go
Normal file
145
internal/api/activitypub/users/outboxget.go
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet
|
||||
//
|
||||
// Get the public outbox collection for an actor.
|
||||
//
|
||||
// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`.
|
||||
//
|
||||
// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`.
|
||||
//
|
||||
// HTTP signature is required on the request.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - s2s/federation
|
||||
//
|
||||
// produces:
|
||||
// - application/activity+json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: username
|
||||
// type: string
|
||||
// description: Username of the account.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// name: page
|
||||
// type: boolean
|
||||
// description: Return response as a CollectionPage.
|
||||
// in: query
|
||||
// default: false
|
||||
// -
|
||||
// name: min_id
|
||||
// type: string
|
||||
// description: Minimum ID of the next status, used for paging.
|
||||
// in: query
|
||||
// -
|
||||
// name: max_id
|
||||
// type: string
|
||||
// description: Maximum ID of the next status, used for paging.
|
||||
// in: query
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/swaggerCollection"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
func (m *Module) OutboxGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
return
|
||||
}
|
||||
|
||||
var page bool
|
||||
if pageString := c.Query(PageKey); pageString != "" {
|
||||
i, err := strconv.ParseBool(pageString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing %s: %s", PageKey, err)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
page = i
|
||||
}
|
||||
|
||||
minID := ""
|
||||
minIDString := c.Query(MinIDKey)
|
||||
if minIDString != "" {
|
||||
minID = minIDString
|
||||
}
|
||||
|
||||
maxID := ""
|
||||
maxIDString := c.Query(MaxIDKey)
|
||||
if maxIDString != "" {
|
||||
maxID = maxIDString
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediOutbox(apiutil.TransferSignatureContext(c), requestedUsername, page, maxID, minID, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
216
internal/api/activitypub/users/outboxget_test.go
Normal file
216
internal/api/activitypub/users/outboxget_test.go
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type OutboxGetTestSuite struct {
|
||||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *OutboxGetTestSuite) TestGetOutbox() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork_outbox"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.userModule.OutboxGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","first":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","id":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollection"}`, string(b))
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
_, ok := t.(vocab.ActivityStreamsOrderedCollection)
|
||||
suite.True(ok)
|
||||
}
|
||||
|
||||
func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork_outbox_first"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
userModule := users.New(processor)
|
||||
suite.NoError(processor.Start())
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true", nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.OutboxGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","next":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":{"actor":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity","object":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY","published":"2021-10-20T10:40:37Z","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"},"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","prev":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026min_id=01F8MHAMCHF6Y650WCRSCP4WMY","type":"OrderedCollectionPage"}`, string(b))
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
_, ok := t.(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
suite.True(ok)
|
||||
}
|
||||
|
||||
func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork_outbox_next"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
userModule := users.New(processor)
|
||||
suite.NoError(processor.Start())
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true&max_id=01F8MHAMCHF6Y650WCRSCP4WMY", nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.MaxIDKey,
|
||||
Value: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.OutboxGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026maxID=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":[],"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollectionPage"}`, string(b))
|
||||
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
_, ok := t.(vocab.ActivityStreamsOrderedCollectionPage)
|
||||
suite.True(ok)
|
||||
}
|
||||
|
||||
func TestOutboxGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(OutboxGetTestSuite))
|
||||
}
|
||||
71
internal/api/activitypub/users/publickeyget.go
Normal file
71
internal/api/activitypub/users/publickeyget.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key.
|
||||
//
|
||||
// The goal here is to return a MINIMAL activitypub representation of an account
|
||||
// in the form of a vocab.ActivityStreamsPerson. The account will only contain the id,
|
||||
// public key, username, and type of the account.
|
||||
func (m *Module) PublicKeyGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
166
internal/api/activitypub/users/repliesget.go
Normal file
166
internal/api/activitypub/users/repliesget.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet
|
||||
//
|
||||
// Get the replies collection for a status.
|
||||
//
|
||||
// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`.
|
||||
//
|
||||
// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`.
|
||||
//
|
||||
// HTTP signature is required on the request.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - s2s/federation
|
||||
//
|
||||
// produces:
|
||||
// - application/activity+json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: username
|
||||
// type: string
|
||||
// description: Username of the account.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// name: status
|
||||
// type: string
|
||||
// description: ID of the status.
|
||||
// in: path
|
||||
// required: true
|
||||
// -
|
||||
// name: page
|
||||
// type: boolean
|
||||
// description: Return response as a CollectionPage.
|
||||
// in: query
|
||||
// default: false
|
||||
// -
|
||||
// name: only_other_accounts
|
||||
// type: boolean
|
||||
// description: Return replies only from accounts other than the status owner.
|
||||
// in: query
|
||||
// default: false
|
||||
// -
|
||||
// name: min_id
|
||||
// type: string
|
||||
// description: Minimum ID of the next status, used for paging.
|
||||
// in: query
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/swaggerCollection"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
func (m *Module) StatusRepliesGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// status IDs on our instance are always uppercase
|
||||
requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
|
||||
if requestedStatusID == "" {
|
||||
err := errors.New("no status id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the status
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
|
||||
return
|
||||
}
|
||||
|
||||
var page bool
|
||||
if pageString := c.Query(PageKey); pageString != "" {
|
||||
i, err := strconv.ParseBool(pageString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing %s: %s", PageKey, err)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
page = i
|
||||
}
|
||||
|
||||
onlyOtherAccounts := false
|
||||
onlyOtherAccountsString := c.Query(OnlyOtherAccountsKey)
|
||||
if onlyOtherAccountsString != "" {
|
||||
i, err := strconv.ParseBool(onlyOtherAccountsString)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
onlyOtherAccounts = i
|
||||
}
|
||||
|
||||
minID := ""
|
||||
minIDString := c.Query(MinIDKey)
|
||||
if minIDString != "" {
|
||||
minID = minIDString
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediStatusReplies(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
239
internal/api/activitypub/users/repliesget_test.go
Normal file
239
internal/api/activitypub/users/repliesget_test.go
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type RepliesGetTestSuite struct {
|
||||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *RepliesGetTestSuite) TestGetReplies() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
targetStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies", nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.StatusIDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.userModule.StatusRepliesGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","first":{"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"},"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"Collection"}`, string(b))
|
||||
|
||||
// should be a Collection
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
_, ok := t.(vocab.ActivityStreamsCollection)
|
||||
assert.True(suite.T(), ok)
|
||||
}
|
||||
|
||||
func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_next"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
targetStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
userModule := users.New(processor)
|
||||
suite.NoError(processor.Start())
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true", nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.StatusIDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.StatusRepliesGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false","items":"http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b))
|
||||
|
||||
// should be a Collection
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
page, ok := t.(vocab.ActivityStreamsCollectionPage)
|
||||
assert.True(suite.T(), ok)
|
||||
|
||||
assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 1)
|
||||
}
|
||||
|
||||
func (suite *RepliesGetTestSuite) TestGetRepliesLast() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_last"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
targetStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker)
|
||||
federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker)
|
||||
emailSender := testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
userModule := users.New(processor)
|
||||
suite.NoError(processor.Start())
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true&min_id=01FF25D5Q0DH7CHD57CTRS6WK0", nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.StatusIDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.StatusRepliesGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
fmt.Println(string(b))
|
||||
assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","items":[],"next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b))
|
||||
|
||||
// should be a Collection
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
assert.NoError(suite.T(), err)
|
||||
|
||||
page, ok := t.(vocab.ActivityStreamsCollectionPage)
|
||||
assert.True(suite.T(), ok)
|
||||
|
||||
assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 0)
|
||||
}
|
||||
|
||||
func TestRepliesGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(RepliesGetTestSuite))
|
||||
}
|
||||
75
internal/api/activitypub/users/statusget.go
Normal file
75
internal/api/activitypub/users/statusget.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it.
|
||||
func (m *Module) StatusGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
// status IDs on our instance are always uppercase
|
||||
requestedStatusID := strings.ToUpper(c.Param(StatusIDKey))
|
||||
if requestedStatusID == "" {
|
||||
err := errors.New("no status id specified in request")
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the status
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediStatus(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
162
internal/api/activitypub/users/statusget_test.go
Normal file
162
internal/api/activitypub/users/statusget_test.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type StatusGetTestSuite struct {
|
||||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *StatusGetTestSuite) TestGetStatus() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
targetStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.StatusIDKey,
|
||||
Value: targetStatus.ID,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.userModule.StatusGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Note
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
note, ok := t.(vocab.ActivityStreamsNote)
|
||||
suite.True(ok)
|
||||
|
||||
// convert note to status
|
||||
a, err := suite.tc.ASStatusToStatus(context.Background(), note)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetStatus.Content, a.Content)
|
||||
}
|
||||
|
||||
func (suite *StatusGetTestSuite) TestGetStatusLowercase() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_lowercase"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
targetStatus := suite.testStatuses["local_account_1_status_1"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, strings.ToLower(targetStatus.URI), nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: strings.ToLower(targetAccount.Username),
|
||||
},
|
||||
gin.Param{
|
||||
Key: users.StatusIDKey,
|
||||
Value: strings.ToLower(targetStatus.ID),
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.userModule.StatusGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Note
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
note, ok := t.(vocab.ActivityStreamsNote)
|
||||
suite.True(ok)
|
||||
|
||||
// convert note to status
|
||||
a, err := suite.tc.ASStatusToStatus(context.Background(), note)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetStatus.Content, a.Content)
|
||||
}
|
||||
|
||||
func TestStatusGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusGetTestSuite))
|
||||
}
|
||||
80
internal/api/activitypub/users/user.go
Normal file
80
internal/api/activitypub/users/user.go
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||
)
|
||||
|
||||
const (
|
||||
// UsernameKey is for account usernames.
|
||||
UsernameKey = "username"
|
||||
// StatusIDKey is for status IDs
|
||||
StatusIDKey = "status"
|
||||
// OnlyOtherAccountsKey is for filtering status responses.
|
||||
OnlyOtherAccountsKey = "only_other_accounts"
|
||||
// MinIDKey is for filtering status responses.
|
||||
MinIDKey = "min_id"
|
||||
// MaxIDKey is for filtering status responses.
|
||||
MaxIDKey = "max_id"
|
||||
// PageKey is for filtering status responses.
|
||||
PageKey = "page"
|
||||
|
||||
// BasePath is the base path for serving AP 'users' requests, minus the 'users' prefix.
|
||||
BasePath = "/:" + UsernameKey
|
||||
// PublicKeyPath is a path to a user's public key, for serving bare minimum AP representations.
|
||||
PublicKeyPath = BasePath + "/" + uris.PublicKeyPath
|
||||
// InboxPath is for serving POST requests to a user's inbox with the given username key.
|
||||
InboxPath = BasePath + "/" + uris.InboxPath
|
||||
// OutboxPath is for serving GET requests to a user's outbox with the given username key.
|
||||
OutboxPath = BasePath + "/" + uris.OutboxPath
|
||||
// FollowersPath is for serving GET request's to a user's followers list, with the given username key.
|
||||
FollowersPath = BasePath + "/" + uris.FollowersPath
|
||||
// FollowingPath is for serving GET request's to a user's following list, with the given username key.
|
||||
FollowingPath = BasePath + "/" + uris.FollowingPath
|
||||
// StatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID
|
||||
StatusPath = BasePath + "/" + uris.StatusesPath + "/:" + StatusIDKey
|
||||
// StatusRepliesPath is for serving the replies collection of a status.
|
||||
StatusRepliesPath = StatusPath + "/replies"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
processor processing.Processor
|
||||
}
|
||||
|
||||
func New(processor processing.Processor) *Module {
|
||||
return &Module{
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, BasePath, m.UsersGETHandler)
|
||||
attachHandler(http.MethodPost, InboxPath, m.InboxPOSTHandler)
|
||||
attachHandler(http.MethodGet, FollowersPath, m.FollowersGETHandler)
|
||||
attachHandler(http.MethodGet, FollowingPath, m.FollowingGETHandler)
|
||||
attachHandler(http.MethodGet, StatusPath, m.StatusGETHandler)
|
||||
attachHandler(http.MethodGet, PublicKeyPath, m.PublicKeyGETHandler)
|
||||
attachHandler(http.MethodGet, StatusRepliesPath, m.StatusRepliesGETHandler)
|
||||
attachHandler(http.MethodGet, OutboxPath, m.OutboxGETHandler)
|
||||
}
|
||||
103
internal/api/activitypub/users/user_test.go
Normal file
103
internal/api/activitypub/users/user_test.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/middleware"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type UserStandardTestSuite struct {
|
||||
// standard suite interfaces
|
||||
suite.Suite
|
||||
db db.DB
|
||||
tc typeutils.TypeConverter
|
||||
mediaManager media.Manager
|
||||
federator federation.Federator
|
||||
emailSender email.Sender
|
||||
processor processing.Processor
|
||||
storage *storage.Driver
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*gtsmodel.Token
|
||||
testClients map[string]*gtsmodel.Client
|
||||
testApplications map[string]*gtsmodel.Application
|
||||
testUsers map[string]*gtsmodel.User
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
testAttachments map[string]*gtsmodel.MediaAttachment
|
||||
testStatuses map[string]*gtsmodel.Status
|
||||
testBlocks map[string]*gtsmodel.Block
|
||||
|
||||
// module being tested
|
||||
userModule *users.Module
|
||||
|
||||
signatureCheck gin.HandlerFunc
|
||||
}
|
||||
|
||||
func (suite *UserStandardTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testClients = testrig.NewTestClients()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testAttachments = testrig.NewTestAttachments()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
suite.testBlocks = testrig.NewTestBlocks()
|
||||
}
|
||||
|
||||
func (suite *UserStandardTestSuite) SetupTest() {
|
||||
testrig.InitTestConfig()
|
||||
testrig.InitTestLog()
|
||||
|
||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||
|
||||
suite.db = testrig.NewTestDB()
|
||||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||
suite.userModule = users.New(suite.processor)
|
||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||
|
||||
suite.signatureCheck = middleware.SignatureCheck(suite.db.IsURIBlocked)
|
||||
|
||||
suite.NoError(suite.processor.Start())
|
||||
}
|
||||
|
||||
func (suite *UserStandardTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
}
|
||||
75
internal/api/activitypub/users/userget.go
Normal file
75
internal/api/activitypub/users/userget.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
// UsersGETHandler should be served at https://example.org/users/:username.
|
||||
//
|
||||
// The goal here is to return the activitypub representation of an account
|
||||
// in the form of a vocab.ActivityStreamsPerson. This should only be served
|
||||
// to REMOTE SERVERS that present a valid signature on the GET request, on
|
||||
// behalf of a user, otherwise we risk leaking information about users publicly.
|
||||
//
|
||||
// And of course, the request should be refused if the account or server making the
|
||||
// request is blocked.
|
||||
func (m *Module) UsersGETHandler(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.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
format, err := apiutil.NegotiateAccept(c, apiutil.HTMLOrActivityPubHeaders...)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
if format == string(apiutil.TextHTML) {
|
||||
// redirect to the user's profile
|
||||
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||
return
|
||||
}
|
||||
|
||||
resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, format, b)
|
||||
}
|
||||
177
internal/api/activitypub/users/userget_test.go
Normal file
177
internal/api/activitypub/users/userget_test.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
GoToSocial
|
||||
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||
|
||||
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 users_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/streams"
|
||||
"github.com/superseriousbusiness/activity/streams/vocab"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type UserGetTestSuite struct {
|
||||
UserStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *UserGetTestSuite) TestGetUser() {
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork"]
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.URI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
suite.userModule.UsersGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Person
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
person, ok := t.(vocab.ActivityStreamsPerson)
|
||||
suite.True(ok)
|
||||
|
||||
// convert person to account
|
||||
// since this account is already known, we should get a pretty full model of it from the conversion
|
||||
a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", false)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetAccount.Username, a.Username)
|
||||
}
|
||||
|
||||
// TestGetUserPublicKeyDeleted checks whether the public key of a deleted account can still be dereferenced.
|
||||
// This is needed by remote instances for authenticating delete requests and stuff like that.
|
||||
func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
|
||||
userModule := users.New(suite.processor)
|
||||
targetAccount := suite.testAccounts["local_account_1"]
|
||||
|
||||
// first delete the account, as though zork had deleted himself
|
||||
authed := &oauth.Auth{
|
||||
Application: suite.testApplications["local_account_1"],
|
||||
User: suite.testUsers["local_account_1"],
|
||||
Account: suite.testAccounts["local_account_1"],
|
||||
}
|
||||
suite.processor.AccountDeleteLocal(context.Background(), authed, &apimodel.AccountDeleteRequest{
|
||||
Password: "password",
|
||||
DeleteOriginID: targetAccount.ID,
|
||||
})
|
||||
|
||||
// wait for the account delete to be processed
|
||||
if !testrig.WaitFor(func() bool {
|
||||
a, _ := suite.db.GetAccountByID(context.Background(), targetAccount.ID)
|
||||
return !a.SuspendedAt.IsZero()
|
||||
}) {
|
||||
suite.FailNow("delete of account timed out")
|
||||
}
|
||||
|
||||
// the dereference we're gonna use
|
||||
derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts)
|
||||
signedRequest := derefRequests["foss_satan_dereference_zork_public_key"]
|
||||
|
||||
// setup request
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.PublicKeyURI, nil) // the endpoint we're hitting
|
||||
ctx.Request.Header.Set("accept", "application/activity+json")
|
||||
ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader)
|
||||
ctx.Request.Header.Set("Date", signedRequest.DateHeader)
|
||||
|
||||
// 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: targetAccount.Username,
|
||||
},
|
||||
}
|
||||
|
||||
// trigger the function being tested
|
||||
userModule.UsersGETHandler(ctx)
|
||||
|
||||
// check response
|
||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
||||
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
b, err := ioutil.ReadAll(result.Body)
|
||||
suite.NoError(err)
|
||||
|
||||
// should be a Person
|
||||
m := make(map[string]interface{})
|
||||
err = json.Unmarshal(b, &m)
|
||||
suite.NoError(err)
|
||||
|
||||
t, err := streams.ToType(context.Background(), m)
|
||||
suite.NoError(err)
|
||||
|
||||
person, ok := t.(vocab.ActivityStreamsPerson)
|
||||
suite.True(ok)
|
||||
|
||||
// convert person to account
|
||||
a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", false)
|
||||
suite.NoError(err)
|
||||
suite.EqualValues(targetAccount.Username, a.Username)
|
||||
}
|
||||
|
||||
func TestUserGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(UserGetTestSuite))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue