delete statuses

This commit is contained in:
tsmethurst 2021-04-18 13:16:51 +02:00
commit ddfb9aae65
20 changed files with 475 additions and 147 deletions

View file

@ -71,7 +71,7 @@
* [ ] Statuses * [ ] Statuses
* [x] /api/v1/statuses POST (Create a new status) * [x] /api/v1/statuses POST (Create a new status)
* [x] /api/v1/statuses/:id GET (View an existing status) * [x] /api/v1/statuses/:id GET (View an existing status)
* [ ] /api/v1/statuses/:id DELETE (Delete a status) * [x] /api/v1/statuses/:id DELETE (Delete a status)
* [ ] /api/v1/statuses/:id/context GET (View statuses above and below status ID) * [ ] /api/v1/statuses/:id/context GET (View statuses above and below status ID)
* [ ] /api/v1/statuses/:id/reblogged_by GET (See who has reblogged a status) * [ ] /api/v1/statuses/:id/reblogged_by GET (See who has reblogged a status)
* [ ] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) * [ ] /api/v1/statuses/:id/favourited_by GET (See who has faved a status)

View file

@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gotosocial" "github.com/superseriousbusiness/gotosocial/internal/gotosocial"
"github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/testrig"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
@ -255,6 +256,19 @@ func main() {
}, },
}, },
}, },
{
Name: "testrig",
Usage: "gotosocial testrig tasks",
Subcommands: []*cli.Command{
{
Name: "start",
Usage: "start the gotosocial testrig",
Action: func(c *cli.Context) error {
return runAction(c, testrig.Run)
},
},
},
},
}, },
} }

View file

@ -32,17 +32,17 @@ import (
) )
const ( const (
accountIDKey = "account_id" AccountIDKey = "account_id"
mediaTypeKey = "media_type" MediaTypeKey = "media_type"
mediaSizeKey = "media_size" MediaSizeKey = "media_size"
fileNameKey = "file_name" FileNameKey = "file_name"
filesPath = "files" FilesPath = "files"
) )
// fileServer implements the RESTAPIModule interface. // FileServer implements the RESTAPIModule interface.
// The goal here is to serve requested media files if the gotosocial server is configured to use local storage. // The goal here is to serve requested media files if the gotosocial server is configured to use local storage.
type fileServer struct { type FileServer struct {
config *config.Config config *config.Config
db db.DB db db.DB
storage storage.Storage storage storage.Storage
@ -52,7 +52,7 @@ type fileServer struct {
// New returns a new fileServer module // New returns a new fileServer module
func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.Logger) apimodule.ClientAPIModule {
return &fileServer{ return &FileServer{
config: config, config: config,
db: db, db: db,
storage: storage, storage: storage,
@ -62,12 +62,12 @@ func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.L
} }
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *fileServer) Route(s router.Router) error { func (m *FileServer) Route(s router.Router) error {
s.AttachHandler(http.MethodGet, fmt.Sprintf("%s/:%s/:%s/:%s/:%s", m.storageBase, accountIDKey, mediaTypeKey, mediaSizeKey, fileNameKey), m.ServeFile) s.AttachHandler(http.MethodGet, fmt.Sprintf("%s/:%s/:%s/:%s/:%s", m.storageBase, AccountIDKey, MediaTypeKey, MediaSizeKey, FileNameKey), m.ServeFile)
return nil return nil
} }
func (m *fileServer) CreateTables(db db.DB) error { func (m *FileServer) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.MediaAttachment{}, &gtsmodel.MediaAttachment{},
} }

View file

@ -33,7 +33,7 @@ import (
// //
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found". // Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything. // Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
func (m *fileServer) ServeFile(c *gin.Context) { func (m *FileServer) ServeFile(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "ServeFile", "func": "ServeFile",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,
@ -45,28 +45,28 @@ func (m *fileServer) ServeFile(c *gin.Context) {
// We use request params to check what to pull out of the database/storage so check everything. A request URL should be formatted as follows: // We use request params to check what to pull out of the database/storage so check everything. A request URL should be formatted as follows:
// "https://example.org/fileserver/[ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]" // "https://example.org/fileserver/[ACCOUNT_ID]/[MEDIA_TYPE]/[MEDIA_SIZE]/[FILE_NAME]"
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension. // "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
accountID := c.Param(accountIDKey) accountID := c.Param(AccountIDKey)
if accountID == "" { if accountID == "" {
l.Debug("missing accountID from request") l.Debug("missing accountID from request")
c.String(http.StatusNotFound, "404 page not found") c.String(http.StatusNotFound, "404 page not found")
return return
} }
mediaType := c.Param(mediaTypeKey) mediaType := c.Param(MediaTypeKey)
if mediaType == "" { if mediaType == "" {
l.Debug("missing mediaType from request") l.Debug("missing mediaType from request")
c.String(http.StatusNotFound, "404 page not found") c.String(http.StatusNotFound, "404 page not found")
return return
} }
mediaSize := c.Param(mediaSizeKey) mediaSize := c.Param(MediaSizeKey)
if mediaSize == "" { if mediaSize == "" {
l.Debug("missing mediaSize from request") l.Debug("missing mediaSize from request")
c.String(http.StatusNotFound, "404 page not found") c.String(http.StatusNotFound, "404 page not found")
return return
} }
fileName := c.Param(fileNameKey) fileName := c.Param(FileNameKey)
if fileName == "" { if fileName == "" {
l.Debug("missing fileName from request") l.Debug("missing fileName from request")
c.String(http.StatusNotFound, "404 page not found") c.String(http.StatusNotFound, "404 page not found")
@ -86,7 +86,7 @@ func (m *fileServer) ServeFile(c *gin.Context) {
c.String(http.StatusNotFound, "404 page not found") c.String(http.StatusNotFound, "404 page not found")
} }
func (m *fileServer) serveAttachment(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) { func (m *FileServer) serveAttachment(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "serveAttachment", "func": "serveAttachment",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,
@ -160,7 +160,7 @@ func (m *fileServer) serveAttachment(c *gin.Context, accountID string, mediaType
c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(attachmentBytes), map[string]string{}) c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(attachmentBytes), map[string]string{})
} }
func (m *fileServer) serveEmoji(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) { func (m *FileServer) serveEmoji(c *gin.Context, accountID string, mediaType string, mediaSize string, fileName string) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "serveEmoji", "func": "serveEmoji",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package fileserver package test
import ( import (
"context" "context"
@ -30,6 +30,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@ -60,7 +61,7 @@ type ServeFileTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// item being tested // item being tested
fileServer *fileServer fileServer *fileserver.FileServer
} }
/* /*
@ -78,7 +79,7 @@ func (suite *ServeFileTestSuite) SetupSuite() {
suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.oauthServer = testrig.NewTestOauthServer(suite.db)
// setup module being tested // setup module being tested
suite.fileServer = New(suite.config, suite.db, suite.storage, suite.log).(*fileServer) suite.fileServer = fileserver.New(suite.config, suite.db, suite.storage, suite.log).(*fileserver.FileServer)
} }
func (suite *ServeFileTestSuite) TearDownSuite() { func (suite *ServeFileTestSuite) TearDownSuite() {
@ -89,7 +90,7 @@ func (suite *ServeFileTestSuite) TearDownSuite() {
func (suite *ServeFileTestSuite) SetupTest() { func (suite *ServeFileTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()
@ -120,19 +121,19 @@ func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() {
// but because we're calling the ServeFile function directly, we need to set them manually. // but because we're calling the ServeFile function directly, we need to set them manually.
ctx.Params = gin.Params{ ctx.Params = gin.Params{
gin.Param{ gin.Param{
Key: accountIDKey, Key: fileserver.AccountIDKey,
Value: targetAttachment.AccountID, Value: targetAttachment.AccountID,
}, },
gin.Param{ gin.Param{
Key: mediaTypeKey, Key: fileserver.MediaTypeKey,
Value: media.MediaAttachment, Value: media.MediaAttachment,
}, },
gin.Param{ gin.Param{
Key: mediaSizeKey, Key: fileserver.MediaSizeKey,
Value: media.MediaOriginal, Value: media.MediaOriginal,
}, },
gin.Param{ gin.Param{
Key: fileNameKey, Key: fileserver.FileNameKey,
Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID), Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID),
}, },
} }

View file

@ -32,9 +32,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
const basePath = "/api/v1/media" const BasePath = "/api/v1/media"
type mediaModule struct { type MediaModule struct {
mediaHandler media.MediaHandler mediaHandler media.MediaHandler
config *config.Config config *config.Config
db db.DB db db.DB
@ -44,7 +44,7 @@ type mediaModule struct {
// New returns a new auth module // New returns a new auth module
func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule { func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, config *config.Config, log *logrus.Logger) apimodule.ClientAPIModule {
return &mediaModule{ return &MediaModule{
mediaHandler: mediaHandler, mediaHandler: mediaHandler,
config: config, config: config,
db: db, db: db,
@ -54,12 +54,12 @@ func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Co
} }
// Route satisfies the RESTAPIModule interface // Route satisfies the RESTAPIModule interface
func (m *mediaModule) Route(s router.Router) error { func (m *MediaModule) Route(s router.Router) error {
s.AttachHandler(http.MethodPost, basePath, m.mediaCreatePOSTHandler) s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler)
return nil return nil
} }
func (m *mediaModule) CreateTables(db db.DB) error { func (m *MediaModule) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.MediaAttachment{}, &gtsmodel.MediaAttachment{},
} }

View file

@ -33,7 +33,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *mediaModule) mediaCreatePOSTHandler(c *gin.Context) { func (m *MediaModule) MediaCreatePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "statusCreatePOSTHandler") l := m.log.WithField("func", "statusCreatePOSTHandler")
authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything* authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything*
if err != nil { if err != nil {

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package media package test
import ( import (
"bytes" "bytes"
@ -36,6 +36,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
@ -63,7 +64,7 @@ type MediaCreateTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// item being tested // item being tested
mediaModule *mediaModule mediaModule *mediamodule.MediaModule
} }
/* /*
@ -81,7 +82,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() {
suite.oauthServer = testrig.NewTestOauthServer(suite.db) suite.oauthServer = testrig.NewTestOauthServer(suite.db)
// setup module being tested // setup module being tested
suite.mediaModule = New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediaModule) suite.mediaModule = mediamodule.New(suite.db, suite.mediaHandler, suite.mastoConverter, suite.config, suite.log).(*mediamodule.MediaModule)
} }
func (suite *MediaCreateTestSuite) TearDownSuite() { func (suite *MediaCreateTestSuite) TearDownSuite() {
@ -92,7 +93,7 @@ func (suite *MediaCreateTestSuite) TearDownSuite() {
func (suite *MediaCreateTestSuite) SetupTest() { func (suite *MediaCreateTestSuite) SetupTest() {
testrig.StandardDBSetup(suite.db) testrig.StandardDBSetup(suite.db)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
suite.testTokens = testrig.NewTestTokens() suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients() suite.testClients = testrig.NewTestClients()
suite.testApplications = testrig.NewTestApplications() suite.testApplications = testrig.NewTestApplications()
@ -129,18 +130,18 @@ func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful()
} }
// create the request // create the request
buf, w, err := testrig.CreateMultipartFormData("file", "../../../testrig/media/test-jpeg.jpg", map[string]string{ buf, w, err := testrig.CreateMultipartFormData("file", "../../../../testrig/media/test-jpeg.jpg", map[string]string{
"description": "this is a test image -- a cool background from somewhere", "description": "this is a test image -- a cool background from somewhere",
"focus": "-0.5,0.5", "focus": "-0.5,0.5",
}) })
if err != nil { if err != nil {
panic(err) panic(err)
} }
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting
ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) ctx.Request.Header.Set("Content-Type", w.FormDataContentType())
// do the actual request // do the actual request
suite.mediaModule.mediaCreatePOSTHandler(ctx) suite.mediaModule.MediaCreatePOSTHandler(ctx)
// check what's in storage *after* the request // check what's in storage *after* the request
storageKeysAfterRequest, err := suite.storage.ListKeys() storageKeysAfterRequest, err := suite.storage.ListKeys()

View file

@ -32,32 +32,30 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/distributor" "github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/mastotypes"
"github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/router" "github.com/superseriousbusiness/gotosocial/internal/router"
) )
const ( const (
idKey = "id" IDKey = "id"
basePath = "/api/v1/statuses" BasePath = "/api/v1/statuses"
basePathWithID = basePath + "/:" + idKey BasePathWithID = BasePath + "/:" + IDKey
contextPath = basePath + "/context" ContextPath = BasePath + "/context"
rebloggedPath = basePath + "/reblogged_by" RebloggedPath = BasePath + "/reblogged_by"
favouritedPath = basePath + "/favourited_by" FavouritedPath = BasePath + "/favourited_by"
favouritePath = basePath + "/favourite" FavouritePath = BasePath + "/favourite"
reblogPath = basePath + "/reblog" ReblogPath = BasePath + "/reblog"
unreblogPath = basePath + "/unreblog" UnreblogPath = BasePath + "/unreblog"
bookmarkPath = basePath + "/bookmark" BookmarkPath = BasePath + "/bookmark"
unbookmarkPath = basePath + "/unbookmark" UnbookmarkPath = BasePath + "/unbookmark"
mutePath = basePath + "/mute" MutePath = BasePath + "/mute"
unmutePath = basePath + "/unmute" UnmutePath = BasePath + "/unmute"
pinPath = basePath + "/pin" PinPath = BasePath + "/pin"
unpinPath = basePath + "/unpin" UnpinPath = BasePath + "/unpin"
) )
type statusModule struct { type StatusModule struct {
config *config.Config config *config.Config
db db.DB db db.DB
oauthServer oauth.Server
mediaHandler media.MediaHandler mediaHandler media.MediaHandler
mastoConverter mastotypes.Converter mastoConverter mastotypes.Converter
distributor distributor.Distributor distributor distributor.Distributor
@ -65,8 +63,8 @@ type statusModule struct {
} }
// New returns a new account module // New returns a new account module
func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule {
return &statusModule{ return &StatusModule{
config: config, config: config,
db: db, db: db,
mediaHandler: mediaHandler, mediaHandler: mediaHandler,
@ -77,13 +75,14 @@ func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler
} }
// Route attaches all routes from this module to the given router // Route attaches all routes from this module to the given router
func (m *statusModule) Route(r router.Router) error { func (m *StatusModule) Route(r router.Router) error {
r.AttachHandler(http.MethodPost, basePath, m.statusCreatePOSTHandler) r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler)
r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler) r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler)
r.AttachHandler(http.MethodDelete, BasePathWithID, m.muxHandler)
return nil return nil
} }
func (m *statusModule) CreateTables(db db.DB) error { func (m *StatusModule) CreateTables(db db.DB) error {
models := []interface{}{ models := []interface{}{
&gtsmodel.User{}, &gtsmodel.User{},
&gtsmodel.Account{}, &gtsmodel.Account{},
@ -111,14 +110,19 @@ func (m *statusModule) CreateTables(db db.DB) error {
return nil return nil
} }
func (m *statusModule) muxHandler(c *gin.Context) { func (m *StatusModule) muxHandler(c *gin.Context) {
m.log.Debug("entering mux handler") m.log.Debug("entering mux handler")
ru := c.Request.RequestURI ru := c.Request.RequestURI
if strings.HasPrefix(ru, contextPath) { if c.Request.Method == http.MethodGet {
// TODO if strings.HasPrefix(ru, ContextPath) {
} else if strings.HasPrefix(ru, rebloggedPath) { // TODO
// TODO } else if strings.HasPrefix(ru, RebloggedPath) {
} else { // TODO
m.statusGETHandler(c) } else {
m.StatusGETHandler(c)
}
}
if c.Request.Method == http.MethodDelete {
m.StatusDELETEHandler(c)
} }
} }

View file

@ -53,7 +53,7 @@ type advancedVisibilityFlagsForm struct {
Likeable *bool `form:"likeable"` Likeable *bool `form:"likeable"`
} }
func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { func (m *StatusModule) StatusCreatePOSTHandler(c *gin.Context) {
l := m.log.WithField("func", "statusCreatePOSTHandler") l := m.log.WithField("func", "statusCreatePOSTHandler")
authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything* authed, err := oauth.MustAuth(c, true, true, true, true) // posting a status is serious business so we want *everything*
if err != nil { if err != nil {
@ -180,10 +180,8 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
Activity: newStatus, Activity: newStatus,
} }
/* // return the frontend representation of the new status to the submitter
FROM THIS POINT ONWARDS WE ARE JUST CREATING THE FRONTEND REPRESENTATION OF THE STATUS TO RETURN TO THE SUBMITTER mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, nil)
*/
mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, newStatus.GTSReplyToStatus)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@ -320,7 +318,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel.
return nil return nil
} }
func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { func (m *StatusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
if form.InReplyToID == "" { if form.InReplyToID == "" {
return nil return nil
} }
@ -369,7 +367,7 @@ func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun
return nil return nil
} }
func (m *statusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error { func (m *StatusModule) parseMediaIDs(form *advancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
if form.MediaIDs == nil { if form.MediaIDs == nil {
return nil return nil
} }
@ -410,7 +408,7 @@ func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string
return nil return nil
} }
func (m *statusModule) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *StatusModule) parseMentions(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
menchies := []string{} menchies := []string{}
gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID)
if err != nil { if err != nil {
@ -429,7 +427,7 @@ func (m *statusModule) parseMentions(form *advancedStatusCreateForm, accountID s
return nil return nil
} }
func (m *statusModule) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *StatusModule) parseTags(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
tags := []string{} tags := []string{}
gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID)
if err != nil { if err != nil {
@ -448,7 +446,7 @@ func (m *statusModule) parseTags(form *advancedStatusCreateForm, accountID strin
return nil return nil
} }
func (m *statusModule) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { func (m *StatusModule) parseEmojis(form *advancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
emojis := []string{} emojis := []string{}
gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID)
if err != nil { if err != nil {

View file

@ -0,0 +1,106 @@
/*
GoToSocial
Copyright (C) 2021 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 status
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (m *StatusModule) StatusDELETEHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{
"func": "StatusDELETEHandler",
"request_uri": c.Request.RequestURI,
"user_agent": c.Request.UserAgent(),
"origin_ip": c.ClientIP(),
})
l.Debugf("entering function")
authed, err := oauth.MustAuth(c, true, false, true, true) // we don't really need an app here but we want everything else
if err != nil {
l.Debug("not authed so can't delete status")
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
return
}
targetStatusID := c.Param(IDKey)
if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
return
}
l.Tracef("going to search for target status %s", targetStatusID)
targetStatus := &gtsmodel.Status{}
if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
l.Errorf("error fetching status %s: %s", targetStatusID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
if targetStatus.AccountID != authed.Account.ID {
l.Debug("status doesn't belong to requesting account")
c.JSON(http.StatusForbidden, gin.H{"error": "not allowed"})
return
}
l.Trace("going to get relevant accounts")
relevantAccounts, err := m.db.PullRelevantAccountsFromStatus(targetStatus)
if err != nil {
l.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
var boostOfStatus *gtsmodel.Status
if targetStatus.BoostOfID != "" {
boostOfStatus = &gtsmodel.Status{}
if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
}
mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
if err != nil {
l.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
if err := m.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
l.Errorf("error deleting status from the database: %s", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
m.distributor.FromClientAPI() <- distributor.FromClientAPI{
APObjectType: gtsmodel.ActivityStreamsNote,
APActivityType: gtsmodel.ActivityStreamsDelete,
Activity: targetStatus,
}
c.JSON(http.StatusOK, mastoStatus)
}

View file

@ -28,7 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/oauth"
) )
func (m *statusModule) statusGETHandler(c *gin.Context) { func (m *StatusModule) StatusGETHandler(c *gin.Context) {
l := m.log.WithFields(logrus.Fields{ l := m.log.WithFields(logrus.Fields{
"func": "statusGETHandler", "func": "statusGETHandler",
"request_uri": c.Request.RequestURI, "request_uri": c.Request.RequestURI,
@ -46,7 +46,7 @@ func (m *statusModule) statusGETHandler(c *gin.Context) {
requestingAccount = authed.Account requestingAccount = authed.Account
} }
targetStatusID := c.Param(idKey) targetStatusID := c.Param(IDKey)
if targetStatusID == "" { if targetStatusID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
return return

View file

@ -31,6 +31,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@ -64,7 +65,7 @@ type StatusCreateTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// module being tested // module being tested
statusModule *statusModule statusModule *status.StatusModule
} }
/* /*
@ -84,7 +85,7 @@ func (suite *StatusCreateTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = New(suite.config, suite.db, suite.oauthServer, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*statusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule)
} }
func (suite *StatusCreateTestSuite) TearDownSuite() { func (suite *StatusCreateTestSuite) TearDownSuite() {
@ -129,7 +130,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"status": {"this is a brand new status! #helloworld"}, "status": {"this is a brand new status! #helloworld"},
"spoiler_text": {"hello hello"}, "spoiler_text": {"hello hello"},
@ -139,7 +140,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
"replyable": {"false"}, "replyable": {"false"},
"federated": {"false"}, "federated": {"false"},
} }
suite.statusModule.statusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)
// check response // check response
@ -183,11 +184,11 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "}, "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
} }
suite.statusModule.statusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)
suite.EqualValues(http.StatusOK, recorder.Code) suite.EqualValues(http.StatusOK, recorder.Code)
@ -224,13 +225,13 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"status": {"this is a reply to a status that doesn't exist"}, "status": {"this is a reply to a status that doesn't exist"},
"spoiler_text": {"don't open cuz it won't work"}, "spoiler_text": {"don't open cuz it won't work"},
"in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"}, "in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"},
} }
suite.statusModule.statusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)
// check response // check response
@ -255,12 +256,12 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)}, "status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)},
"in_reply_to_id": {testrig.NewTestStatuses()["local_account_2_status_1"].ID}, "in_reply_to_id": {testrig.NewTestStatuses()["local_account_2_status_1"].ID},
} }
suite.statusModule.statusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusOK, recorder.Code) suite.EqualValues(http.StatusOK, recorder.Code)
@ -295,12 +296,12 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() {
ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting
ctx.Request.Form = url.Values{ ctx.Request.Form = url.Values{
"status": {"here's an image attachment"}, "status": {"here's an image attachment"},
"media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"}, "media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"},
} }
suite.statusModule.statusCreatePOSTHandler(ctx) suite.statusModule.StatusCreatePOSTHandler(ctx)
// check response // check response
suite.EqualValues(http.StatusOK, recorder.Code) suite.EqualValues(http.StatusOK, recorder.Code)

View file

@ -23,6 +23,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
"github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
@ -55,7 +56,7 @@ type StatusGetTestSuite struct {
testAttachments map[string]*gtsmodel.MediaAttachment testAttachments map[string]*gtsmodel.MediaAttachment
// module being tested // module being tested
statusModule *statusModule statusModule *status.StatusModule
} }
/* /*
@ -75,7 +76,7 @@ func (suite *StatusGetTestSuite) SetupSuite() {
suite.distributor = testrig.NewTestDistributor() suite.distributor = testrig.NewTestDistributor()
// setup module being tested // setup module being tested
suite.statusModule = New(suite.config, suite.db, suite.oauthServer, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*statusModule) suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule)
} }
func (suite *StatusGetTestSuite) TearDownSuite() { func (suite *StatusGetTestSuite) TearDownSuite() {

View file

@ -60,12 +60,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
} }
log.Debugf("using pg options: %+v", opts) log.Debugf("using pg options: %+v", opts)
readyChan := make(chan interface{})
opts.OnConnect = func(ctx context.Context, c *pg.Conn) error {
close(readyChan)
return nil
}
// create a connection // create a connection
pgCtx, cancel := context.WithCancel(ctx) pgCtx, cancel := context.WithCancel(ctx)
conn := pg.Connect(opts).WithContext(pgCtx) conn := pg.Connect(opts).WithContext(pgCtx)
@ -80,8 +74,7 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
}) })
} }
// actually *begin* the connection so that we can tell if the db is there // actually *begin* the connection so that we can tell if the db is there and listening
// and listening, and also trigger the opts.OnConnect function passed in above
if err := conn.Ping(ctx); err != nil { if err := conn.Ping(ctx); err != nil {
cancel() cancel()
return nil, fmt.Errorf("db connection error: %s", err) return nil, fmt.Errorf("db connection error: %s", err)
@ -95,16 +88,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry
} }
log.Infof("connected to postgres version: %s", version) log.Infof("connected to postgres version: %s", version)
// make sure the opts.OnConnect function has been triggered
// and closed the ready channel
select {
case <-readyChan:
log.Infof("postgres connection ready")
case <-time.After(5 * time.Second):
cancel()
return nil, errors.New("db connection timeout")
}
ps := &postgresService{ ps := &postgresService{
config: c, config: c,
conn: conn, conn: conn,
@ -585,15 +568,18 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed. // If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed.
// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't.
if requestingAccount == nil { if requestingAccount == nil {
if targetStatus.Visibility == gtsmodel.VisibilityPublic { if targetStatus.Visibility == gtsmodel.VisibilityPublic {
return true, nil return true, nil
} }
l.Debug("requesting account is nil but the target status isn't public")
return false, nil return false, nil
} }
// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten // if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten
// this far (ie., been authed) in the first place: this is just for safety. // this far (ie., been authed) in the first place: this is just for safety.
if !requestingAccount.SuspendedAt.IsZero() { if !requestingAccount.SuspendedAt.IsZero() {
l.Debug("requesting account is suspended")
return false, nil return false, nil
} }
@ -603,24 +589,33 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil { if err := ps.conn.Model(requestingUser).Where("account_id = ?", requestingAccount.ID).Select(); err != nil {
// if the requesting account is local but doesn't have a corresponding user in the db this is a problem // if the requesting account is local but doesn't have a corresponding user in the db this is a problem
if err == pg.ErrNoRows { if err == pg.ErrNoRows {
l.Debug("requesting account is local but there's no corresponding user")
return false, nil return false, nil
} else { } else {
l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err)
return false, err return false, err
} }
} }
// okay, user exists, so make sure it has full privileges/is confirmed/approved // okay, user exists, so make sure it has full privileges/is confirmed/approved
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() { if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
return false, nil return false, nil
} }
} }
// if the target status belongs to the requesting account, they should always be able to view it at this point
if targetStatus.AccountID == requestingAccount.ID {
return true, nil
}
// At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou // At this point we have a populated targetAccount, targetStatus, and requestingAccount, so we can check for blocks and whathaveyou
// First check if a block exists directly between the target account (which authored the status) and the requesting account. // First check if a block exists directly between the target account (which authored the status) and the requesting account.
if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil { if blocked, err := ps.Blocked(targetAccount.ID, requestingAccount.ID); err != nil {
// something went wrong figuring out if the accounts have a block l.Debug("something went wrong figuring out if the accounts have a block: %s", err)
return false, err return false, err
} else if blocked { } else if blocked {
// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please // don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please
l.Debug("a block exists between requesting account and target account")
return false, nil return false, nil
} }

View file

@ -83,7 +83,7 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
mm := mediaModule.New(dbService, mediaHandler, mastoConverter, c, log) mm := mediaModule.New(dbService, mediaHandler, mastoConverter, c, log)
fileServerModule := fileserver.New(c, dbService, storageBackend, log) fileServerModule := fileserver.New(c, dbService, storageBackend, log)
adminModule := admin.New(c, dbService, mediaHandler, mastoConverter, log) adminModule := admin.New(c, dbService, mediaHandler, mastoConverter, log)
statusModule := status.New(c, dbService, oauthServer, mediaHandler, mastoConverter, distributor, log) statusModule := status.New(c, dbService, mediaHandler, mastoConverter, distributor, log)
securityModule := security.New(c, log) securityModule := security.New(c, log)
apiModules := []apimodule.ClientAPIModule{ apiModules := []apimodule.ClientAPIModule{

View file

@ -345,40 +345,53 @@ func (c *converter) StatusToMasto(
return nil, fmt.Errorf("error counting faves: %s", err) return nil, fmt.Errorf("error counting faves: %s", err)
} }
faved, err := c.db.StatusFavedBy(s, requestingAccount.ID) var faved bool
if err != nil { var reblogged bool
return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err) var bookmarked bool
} var pinned bool
var muted bool
reblogged, err := c.db.StatusRebloggedBy(s, requestingAccount.ID) // requestingAccount will be nil for public requests without auth
if err != nil { // But if it's not nil, we can also get information about the requestingAccount's interaction with this status
return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) if requestingAccount != nil {
} faved, err = c.db.StatusFavedBy(s, requestingAccount.ID)
if err != nil {
return nil, fmt.Errorf("error checking if requesting account has faved status: %s", err)
}
muted, err := c.db.StatusMutedBy(s, requestingAccount.ID) reblogged, err = c.db.StatusRebloggedBy(s, requestingAccount.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err) return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err)
} }
bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID) muted, err = c.db.StatusMutedBy(s, requestingAccount.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err) return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err)
} }
pinned, err := c.db.StatusPinnedBy(s, requestingAccount.ID) bookmarked, err = c.db.StatusBookmarkedBy(s, requestingAccount.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error checking if requesting account has pinned status: %s", err) return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err)
}
pinned, err = c.db.StatusPinnedBy(s, requestingAccount.ID)
if err != nil {
return nil, fmt.Errorf("error checking if requesting account has pinned status: %s", err)
}
} }
var mastoRebloggedStatus *mastotypes.Status // TODO var mastoRebloggedStatus *mastotypes.Status // TODO
application := &gtsmodel.Application{} var mastoApplication *mastotypes.Application
if err := c.db.GetByID(s.CreatedWithApplicationID, application); err != nil { if s.CreatedWithApplicationID != "" {
return nil, fmt.Errorf("error fetching application used to create status: %s", err) gtsApplication := &gtsmodel.Application{}
} if err := c.db.GetByID(s.CreatedWithApplicationID, gtsApplication); err != nil {
mastoApplication, err := c.AppToMastoPublic(application) return nil, fmt.Errorf("error fetching application used to create status: %s", err)
if err != nil { }
return nil, fmt.Errorf("error parsing application used to create status: %s", err) mastoApplication, err = c.AppToMastoPublic(gtsApplication)
if err != nil {
return nil, fmt.Errorf("error parsing application used to create status: %s", err)
}
} }
mastoTargetAccount, err := c.AccountToMastoPublic(targetAccount) mastoTargetAccount, err := c.AccountToMastoPublic(targetAccount)

125
testrig/actions.go Normal file
View file

@ -0,0 +1,125 @@
/*
GoToSocial
Copyright (C) 2021 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 testrig
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/action"
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/account"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/admin"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/app"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/auth"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver"
mediaModule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/security"
"github.com/superseriousbusiness/gotosocial/internal/apimodule/status"
"github.com/superseriousbusiness/gotosocial/internal/cache"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/gotosocial"
)
// Run creates and starts a gotosocial testrig server
var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logrus.Logger) error {
dbService := NewTestDB()
router := NewTestRouter()
storageBackend := NewTestStorage()
mediaHandler := NewTestMediaHandler(dbService, storageBackend)
oauthServer := NewTestOauthServer(dbService)
distributor := NewTestDistributor()
if err := distributor.Start(); err != nil {
return fmt.Errorf("error starting distributor: %s", err)
}
mastoConverter := NewTestMastoConverter(dbService)
c := NewTestConfig()
StandardDBSetup(dbService)
StandardStorageSetup(storageBackend, "./testrig/media")
// build client api modules
authModule := auth.New(oauthServer, dbService, log)
accountModule := account.New(c, dbService, oauthServer, mediaHandler, mastoConverter, log)
appsModule := app.New(oauthServer, dbService, mastoConverter, log)
mm := mediaModule.New(dbService, mediaHandler, mastoConverter, c, log)
fileServerModule := fileserver.New(c, dbService, storageBackend, log)
adminModule := admin.New(c, dbService, mediaHandler, mastoConverter, log)
statusModule := status.New(c, dbService, mediaHandler, mastoConverter, distributor, log)
securityModule := security.New(c, log)
apiModules := []apimodule.ClientAPIModule{
// modules with middleware go first
securityModule,
authModule,
// now everything else
accountModule,
appsModule,
mm,
fileServerModule,
adminModule,
statusModule,
}
for _, m := range apiModules {
if err := m.Route(router); err != nil {
return fmt.Errorf("routing error: %s", err)
}
if err := m.CreateTables(dbService); err != nil {
return fmt.Errorf("table creation error: %s", err)
}
}
// if err := dbService.CreateInstanceAccount(); err != nil {
// return fmt.Errorf("error creating instance account: %s", err)
// }
gts, err := gotosocial.New(dbService, &cache.MockCache{}, router, federation.New(dbService, log), c)
if err != nil {
return fmt.Errorf("error creating gotosocial service: %s", err)
}
if err := gts.Start(ctx); err != nil {
return fmt.Errorf("error starting gotosocial service: %s", err)
}
// catch shutdown signals from the operating system
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
sig := <-sigs
log.Infof("received signal %s, shutting down", sig)
StandardDBTeardown(dbService)
StandardStorageTeardown(storageBackend)
// close down all running services in order
if err := gts.Stop(ctx); err != nil {
return fmt.Errorf("error closing gotosocial service: %s", err)
}
log.Info("done! exiting...")
return nil
}

29
testrig/router.go Normal file
View file

@ -0,0 +1,29 @@
/*
GoToSocial
Copyright (C) 2021 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 testrig
import "github.com/superseriousbusiness/gotosocial/internal/router"
func NewTestRouter() router.Router {
r, err := router.New(NewTestConfig(), NewTestLog())
if err != nil {
panic(err)
}
return r
}

View file

@ -48,12 +48,24 @@ func NewTestTokens() map[string]*oauth.Token {
// NewTestClients returns a map of Clients keyed according to which account they are used by. // NewTestClients returns a map of Clients keyed according to which account they are used by.
func NewTestClients() map[string]*oauth.Client { func NewTestClients() map[string]*oauth.Client {
clients := map[string]*oauth.Client{ clients := map[string]*oauth.Client{
"admin_account": {
ID: "1c5cefc8-f0c9-4307-8506-ca6e3888675e",
Secret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a",
Domain: "http://localhost:8080",
UserID: "0fb02eae-2214-473f-9667-0a43f22d75ff", // admin_account
},
"local_account_1": { "local_account_1": {
ID: "73b48d42-029d-4487-80fc-329a5cf67869", ID: "73b48d42-029d-4487-80fc-329a5cf67869",
Secret: "c3724c74-dc3b-41b2-a108-0ea3d8399830", Secret: "c3724c74-dc3b-41b2-a108-0ea3d8399830",
Domain: "http://localhost:8080", Domain: "http://localhost:8080",
UserID: "44e36b79-44a4-4bd8-91e9-097f477fe97b", // local_account_1 UserID: "44e36b79-44a4-4bd8-91e9-097f477fe97b", // local_account_1
}, },
"local_account_2": {
ID: "a4f6a2ea-a32b-4600-8853-72fc4ad98a1f",
Secret: "8f5603a5-c721-46cd-8f1b-2e368f51379f",
Domain: "http://localhost:8080",
UserID: "d120bd97-866f-4a05-9690-a1294b9934c3", // local_account_2
},
} }
return clients return clients
} }
@ -61,6 +73,16 @@ func NewTestClients() map[string]*oauth.Client {
// NewTestApplications returns a map of applications keyed to which number application they are. // NewTestApplications returns a map of applications keyed to which number application they are.
func NewTestApplications() map[string]*gtsmodel.Application { func NewTestApplications() map[string]*gtsmodel.Application {
apps := map[string]*gtsmodel.Application{ apps := map[string]*gtsmodel.Application{
"admin_account": {
ID: "9bf9e368-037f-444d-8ffd-1091d1c21c4c",
Name: "superseriousbusiness",
Website: "https://superserious.business",
RedirectURI: "http://localhost:8080",
ClientID: "1c5cefc8-f0c9-4307-8506-ca6e3888675e", // admin client
ClientSecret: "dda8e835-2c9c-4bd2-9b8b-77c2e26d7a7a", // admin client
Scopes: "read write follow push",
VapidKey: "76ae0095-8a10-438f-9f49-522d1985b190",
},
"application_1": { "application_1": {
ID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", ID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc",
Name: "really cool gts application", Name: "really cool gts application",
@ -71,6 +93,16 @@ func NewTestApplications() map[string]*gtsmodel.Application {
Scopes: "read write follow push", Scopes: "read write follow push",
VapidKey: "4738dfd7-ca73-4aa6-9aa9-80e946b7db36", VapidKey: "4738dfd7-ca73-4aa6-9aa9-80e946b7db36",
}, },
"application_2": {
ID: "6b0cd164-8497-4cd5-bec9-957886fac5df",
Name: "kindaweird",
Website: "https://kindaweird.app",
RedirectURI: "http://localhost:8080",
ClientID: "a4f6a2ea-a32b-4600-8853-72fc4ad98a1f", // client_2
ClientSecret: "8f5603a5-c721-46cd-8f1b-2e368f51379f", // client_2
Scopes: "read write follow push",
VapidKey: "c040a5fc-e1e2-4859-bbea-0a3efbca1c4b",
},
} }
return apps return apps
} }
@ -128,7 +160,7 @@ func NewTestUsers() map[string]*gtsmodel.User {
CreatedByApplicationID: "", CreatedByApplicationID: "",
LastEmailedAt: time.Now().Add(-30 * time.Minute), LastEmailedAt: time.Now().Add(-30 * time.Minute),
ConfirmationToken: "", ConfirmationToken: "",
ConfirmedAt: time.Time{}, ConfirmedAt: time.Now().Add(-72 * time.Hour),
ConfirmationSentAt: time.Time{}, ConfirmationSentAt: time.Time{},
UnconfirmedEmail: "", UnconfirmedEmail: "",
Moderator: true, Moderator: true,
@ -689,13 +721,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
CreatedAt: time.Now().Add(-71 * time.Hour), CreatedAt: time.Now().Add(-71 * time.Hour),
UpdatedAt: time.Now().Add(-71 * time.Hour), UpdatedAt: time.Now().Add(-71 * time.Hour),
Local: true, Local: true,
AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f",
InReplyToID: "", InReplyToID: "",
BoostOfID: "", BoostOfID: "",
ContentWarning: "", ContentWarning: "",
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Sensitive: false, Sensitive: false,
Language: "en", Language: "en",
CreatedWithApplicationID: "9bf9e368-037f-444d-8ffd-1091d1c21c4c",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,
@ -712,13 +745,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
CreatedAt: time.Now().Add(-70 * time.Hour), CreatedAt: time.Now().Add(-70 * time.Hour),
UpdatedAt: time.Now().Add(-70 * time.Hour), UpdatedAt: time.Now().Add(-70 * time.Hour),
Local: true, Local: true,
AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f",
InReplyToID: "", InReplyToID: "",
BoostOfID: "", BoostOfID: "",
ContentWarning: "open to see some puppies", ContentWarning: "open to see some puppies",
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Sensitive: true, Sensitive: true,
Language: "en", Language: "en",
CreatedWithApplicationID: "9bf9e368-037f-444d-8ffd-1091d1c21c4c",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,
@ -742,6 +776,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Sensitive: true, Sensitive: true,
Language: "en", Language: "en",
CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,
@ -765,6 +800,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityUnlocked, Visibility: gtsmodel.VisibilityUnlocked,
Sensitive: false, Sensitive: false,
Language: "en", Language: "en",
CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: false, Federated: false,
Boostable: true, Boostable: true,
@ -788,6 +824,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityMutualsOnly, Visibility: gtsmodel.VisibilityMutualsOnly,
Sensitive: false, Sensitive: false,
Language: "en", Language: "en",
CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: false, Boostable: false,
@ -812,6 +849,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityMutualsOnly, Visibility: gtsmodel.VisibilityMutualsOnly,
Sensitive: false, Sensitive: false,
Language: "en", Language: "en",
CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,
@ -835,6 +873,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Sensitive: true, Sensitive: true,
Language: "en", Language: "en",
CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,
@ -858,6 +897,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
Visibility: gtsmodel.VisibilityPublic, Visibility: gtsmodel.VisibilityPublic,
Sensitive: true, Sensitive: true,
Language: "en", Language: "en",
CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df",
VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{ VisibilityAdvanced: &gtsmodel.VisibilityAdvanced{
Federated: true, Federated: true,
Boostable: true, Boostable: true,