diff --git a/PROGRESS.md b/PROGRESS.md index 7010da5ec..013ad080a 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -71,7 +71,7 @@ * [ ] Statuses * [x] /api/v1/statuses POST (Create a new 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/reblogged_by GET (See who has reblogged a status) * [ ] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) diff --git a/cmd/gotosocial/main.go b/cmd/gotosocial/main.go index 43d2d2ac7..337e7b785 100644 --- a/cmd/gotosocial/main.go +++ b/cmd/gotosocial/main.go @@ -28,6 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gotosocial" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/testrig" "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) + }, + }, + }, + }, }, } diff --git a/internal/apimodule/fileserver/fileserver.go b/internal/apimodule/fileserver/fileserver.go index bc595734f..25f3be864 100644 --- a/internal/apimodule/fileserver/fileserver.go +++ b/internal/apimodule/fileserver/fileserver.go @@ -32,17 +32,17 @@ import ( ) const ( - accountIDKey = "account_id" - mediaTypeKey = "media_type" - mediaSizeKey = "media_size" - fileNameKey = "file_name" + AccountIDKey = "account_id" + MediaTypeKey = "media_type" + MediaSizeKey = "media_size" + 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. -type fileServer struct { +type FileServer struct { config *config.Config db db.DB storage storage.Storage @@ -52,7 +52,7 @@ type fileServer struct { // New returns a new fileServer module func New(config *config.Config, db db.DB, storage storage.Storage, log *logrus.Logger) apimodule.ClientAPIModule { - return &fileServer{ + return &FileServer{ config: config, db: db, 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 -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) +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) return nil } -func (m *fileServer) CreateTables(db db.DB) error { +func (m *FileServer) CreateTables(db db.DB) error { models := []interface{}{ >smodel.MediaAttachment{}, } diff --git a/internal/apimodule/fileserver/servefile.go b/internal/apimodule/fileserver/servefile.go index 66bc18542..573ad0694 100644 --- a/internal/apimodule/fileserver/servefile.go +++ b/internal/apimodule/fileserver/servefile.go @@ -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". // 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{ "func": "ServeFile", "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: // "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. - accountID := c.Param(accountIDKey) + accountID := c.Param(AccountIDKey) if accountID == "" { l.Debug("missing accountID from request") c.String(http.StatusNotFound, "404 page not found") return } - mediaType := c.Param(mediaTypeKey) + mediaType := c.Param(MediaTypeKey) if mediaType == "" { l.Debug("missing mediaType from request") c.String(http.StatusNotFound, "404 page not found") return } - mediaSize := c.Param(mediaSizeKey) + mediaSize := c.Param(MediaSizeKey) if mediaSize == "" { l.Debug("missing mediaSize from request") c.String(http.StatusNotFound, "404 page not found") return } - fileName := c.Param(fileNameKey) + fileName := c.Param(FileNameKey) if fileName == "" { l.Debug("missing fileName from request") 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") } -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{ "func": "serveAttachment", "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{}) } -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{ "func": "serveEmoji", "request_uri": c.Request.RequestURI, diff --git a/internal/apimodule/fileserver/servefile_test.go b/internal/apimodule/fileserver/test/servefile_test.go similarity index 91% rename from internal/apimodule/fileserver/servefile_test.go rename to internal/apimodule/fileserver/test/servefile_test.go index 9b744a8f6..8af2b40b3 100644 --- a/internal/apimodule/fileserver/servefile_test.go +++ b/internal/apimodule/fileserver/test/servefile_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package fileserver +package test import ( "context" @@ -30,6 +30,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/apimodule/fileserver" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" @@ -60,7 +61,7 @@ type ServeFileTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment // item being tested - fileServer *fileServer + fileServer *fileserver.FileServer } /* @@ -78,7 +79,7 @@ func (suite *ServeFileTestSuite) SetupSuite() { suite.oauthServer = testrig.NewTestOauthServer(suite.db) // 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() { @@ -89,7 +90,7 @@ func (suite *ServeFileTestSuite) TearDownSuite() { func (suite *ServeFileTestSuite) SetupTest() { testrig.StandardDBSetup(suite.db) - testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") + testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") suite.testTokens = testrig.NewTestTokens() suite.testClients = testrig.NewTestClients() 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. ctx.Params = gin.Params{ gin.Param{ - Key: accountIDKey, + Key: fileserver.AccountIDKey, Value: targetAttachment.AccountID, }, gin.Param{ - Key: mediaTypeKey, + Key: fileserver.MediaTypeKey, Value: media.MediaAttachment, }, gin.Param{ - Key: mediaSizeKey, + Key: fileserver.MediaSizeKey, Value: media.MediaOriginal, }, gin.Param{ - Key: fileNameKey, + Key: fileserver.FileNameKey, Value: fmt.Sprintf("%s.jpeg", targetAttachment.ID), }, } diff --git a/internal/apimodule/media/media.go b/internal/apimodule/media/media.go index 44848a3f2..c8d3d7425 100644 --- a/internal/apimodule/media/media.go +++ b/internal/apimodule/media/media.go @@ -32,9 +32,9 @@ import ( "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 config *config.Config db db.DB @@ -44,7 +44,7 @@ type mediaModule struct { // 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 { - return &mediaModule{ + return &MediaModule{ mediaHandler: mediaHandler, config: config, db: db, @@ -54,12 +54,12 @@ func New(db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Co } // Route satisfies the RESTAPIModule interface -func (m *mediaModule) Route(s router.Router) error { - s.AttachHandler(http.MethodPost, basePath, m.mediaCreatePOSTHandler) +func (m *MediaModule) Route(s router.Router) error { + s.AttachHandler(http.MethodPost, BasePath, m.MediaCreatePOSTHandler) return nil } -func (m *mediaModule) CreateTables(db db.DB) error { +func (m *MediaModule) CreateTables(db db.DB) error { models := []interface{}{ >smodel.MediaAttachment{}, } diff --git a/internal/apimodule/media/mediacreate.go b/internal/apimodule/media/mediacreate.go index f082e9bf4..06b6d5be6 100644 --- a/internal/apimodule/media/mediacreate.go +++ b/internal/apimodule/media/mediacreate.go @@ -33,7 +33,7 @@ import ( "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") authed, err := oauth.MustAuth(c, true, true, true, true) // posting new media is serious business so we want *everything* if err != nil { diff --git a/internal/apimodule/media/mediacreate_test.go b/internal/apimodule/media/test/mediacreate_test.go similarity index 91% rename from internal/apimodule/media/mediacreate_test.go rename to internal/apimodule/media/test/mediacreate_test.go index ab77a9a3d..33215aea9 100644 --- a/internal/apimodule/media/mediacreate_test.go +++ b/internal/apimodule/media/test/mediacreate_test.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package media +package test import ( "bytes" @@ -36,6 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mediamodule "github.com/superseriousbusiness/gotosocial/internal/apimodule/media" mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -63,7 +64,7 @@ type MediaCreateTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment // item being tested - mediaModule *mediaModule + mediaModule *mediamodule.MediaModule } /* @@ -81,7 +82,7 @@ func (suite *MediaCreateTestSuite) SetupSuite() { suite.oauthServer = testrig.NewTestOauthServer(suite.db) // 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() { @@ -92,7 +93,7 @@ func (suite *MediaCreateTestSuite) TearDownSuite() { func (suite *MediaCreateTestSuite) SetupTest() { testrig.StandardDBSetup(suite.db) - testrig.StandardStorageSetup(suite.storage, "../../../testrig/media") + testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") suite.testTokens = testrig.NewTestTokens() suite.testClients = testrig.NewTestClients() suite.testApplications = testrig.NewTestApplications() @@ -129,18 +130,18 @@ func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() } // 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", "focus": "-0.5,0.5", }) if err != nil { 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()) // do the actual request - suite.mediaModule.mediaCreatePOSTHandler(ctx) + suite.mediaModule.MediaCreatePOSTHandler(ctx) // check what's in storage *after* the request storageKeysAfterRequest, err := suite.storage.ListKeys() diff --git a/internal/apimodule/status/status.go b/internal/apimodule/status/status.go index cb4dab134..37e6ee268 100644 --- a/internal/apimodule/status/status.go +++ b/internal/apimodule/status/status.go @@ -32,32 +32,30 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/distributor" "github.com/superseriousbusiness/gotosocial/internal/mastotypes" "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" ) const ( - idKey = "id" - basePath = "/api/v1/statuses" - basePathWithID = basePath + "/:" + idKey - contextPath = basePath + "/context" - rebloggedPath = basePath + "/reblogged_by" - favouritedPath = basePath + "/favourited_by" - favouritePath = basePath + "/favourite" - reblogPath = basePath + "/reblog" - unreblogPath = basePath + "/unreblog" - bookmarkPath = basePath + "/bookmark" - unbookmarkPath = basePath + "/unbookmark" - mutePath = basePath + "/mute" - unmutePath = basePath + "/unmute" - pinPath = basePath + "/pin" - unpinPath = basePath + "/unpin" + IDKey = "id" + BasePath = "/api/v1/statuses" + BasePathWithID = BasePath + "/:" + IDKey + ContextPath = BasePath + "/context" + RebloggedPath = BasePath + "/reblogged_by" + FavouritedPath = BasePath + "/favourited_by" + FavouritePath = BasePath + "/favourite" + ReblogPath = BasePath + "/reblog" + UnreblogPath = BasePath + "/unreblog" + BookmarkPath = BasePath + "/bookmark" + UnbookmarkPath = BasePath + "/unbookmark" + MutePath = BasePath + "/mute" + UnmutePath = BasePath + "/unmute" + PinPath = BasePath + "/pin" + UnpinPath = BasePath + "/unpin" ) -type statusModule struct { +type StatusModule struct { config *config.Config db db.DB - oauthServer oauth.Server mediaHandler media.MediaHandler mastoConverter mastotypes.Converter distributor distributor.Distributor @@ -65,8 +63,8 @@ type statusModule struct { } // 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 { - return &statusModule{ +func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, mastoConverter mastotypes.Converter, distributor distributor.Distributor, log *logrus.Logger) apimodule.ClientAPIModule { + return &StatusModule{ config: config, db: db, 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 -func (m *statusModule) Route(r router.Router) error { - r.AttachHandler(http.MethodPost, basePath, m.statusCreatePOSTHandler) - r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler) +func (m *StatusModule) Route(r router.Router) error { + r.AttachHandler(http.MethodPost, BasePath, m.StatusCreatePOSTHandler) + r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) + r.AttachHandler(http.MethodDelete, BasePathWithID, m.muxHandler) return nil } -func (m *statusModule) CreateTables(db db.DB) error { +func (m *StatusModule) CreateTables(db db.DB) error { models := []interface{}{ >smodel.User{}, >smodel.Account{}, @@ -111,14 +110,19 @@ func (m *statusModule) CreateTables(db db.DB) error { return nil } -func (m *statusModule) muxHandler(c *gin.Context) { +func (m *StatusModule) muxHandler(c *gin.Context) { m.log.Debug("entering mux handler") ru := c.Request.RequestURI - if strings.HasPrefix(ru, contextPath) { - // TODO - } else if strings.HasPrefix(ru, rebloggedPath) { - // TODO - } else { - m.statusGETHandler(c) + if c.Request.Method == http.MethodGet { + if strings.HasPrefix(ru, ContextPath) { + // TODO + } else if strings.HasPrefix(ru, RebloggedPath) { + // TODO + } else { + m.StatusGETHandler(c) + } + } + if c.Request.Method == http.MethodDelete { + m.StatusDELETEHandler(c) } } diff --git a/internal/apimodule/status/statuscreate.go b/internal/apimodule/status/statuscreate.go index 0f5b00200..ce1cc6da7 100644 --- a/internal/apimodule/status/statuscreate.go +++ b/internal/apimodule/status/statuscreate.go @@ -53,7 +53,7 @@ type advancedVisibilityFlagsForm struct { Likeable *bool `form:"likeable"` } -func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { +func (m *StatusModule) StatusCreatePOSTHandler(c *gin.Context) { 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* if err != nil { @@ -180,10 +180,8 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { Activity: newStatus, } - /* - 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, newStatus.GTSReplyToStatus) + // return the frontend representation of the new status to the submitter + mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, nil) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -320,7 +318,7 @@ func parseVisibility(form *advancedStatusCreateForm, accountDefaultVis gtsmodel. 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 == "" { return nil } @@ -369,7 +367,7 @@ func (m *statusModule) parseReplyToID(form *advancedStatusCreateForm, thisAccoun 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 { return nil } @@ -410,7 +408,7 @@ func parseLanguage(form *advancedStatusCreateForm, accountDefaultLanguage string 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{} gtsMenchies, err := m.db.MentionStringsToMentions(util.DeriveMentions(form.Status), accountID, status.ID) if err != nil { @@ -429,7 +427,7 @@ func (m *statusModule) parseMentions(form *advancedStatusCreateForm, accountID s 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{} gtsTags, err := m.db.TagStringsToTags(util.DeriveHashtags(form.Status), accountID, status.ID) if err != nil { @@ -448,7 +446,7 @@ func (m *statusModule) parseTags(form *advancedStatusCreateForm, accountID strin 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{} gtsEmojis, err := m.db.EmojiStringsToEmojis(util.DeriveEmojis(form.Status), accountID, status.ID) if err != nil { diff --git a/internal/apimodule/status/statusdelete.go b/internal/apimodule/status/statusdelete.go new file mode 100644 index 000000000..85b10ff16 --- /dev/null +++ b/internal/apimodule/status/statusdelete.go @@ -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 . +*/ + +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 := >smodel.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 = >smodel.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) +} diff --git a/internal/apimodule/status/statusget.go b/internal/apimodule/status/statusget.go index 4d1e3abbb..ed2e89159 100644 --- a/internal/apimodule/status/statusget.go +++ b/internal/apimodule/status/statusget.go @@ -28,7 +28,7 @@ import ( "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{ "func": "statusGETHandler", "request_uri": c.Request.RequestURI, @@ -46,7 +46,7 @@ func (m *statusModule) statusGETHandler(c *gin.Context) { requestingAccount = authed.Account } - targetStatusID := c.Param(idKey) + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) return diff --git a/internal/apimodule/status/statuscreate_test.go b/internal/apimodule/status/test/statuscreate_test.go similarity index 93% rename from internal/apimodule/status/statuscreate_test.go rename to internal/apimodule/status/test/statuscreate_test.go index 8439b694a..6c5aa6b7d 100644 --- a/internal/apimodule/status/statuscreate_test.go +++ b/internal/apimodule/status/test/statuscreate_test.go @@ -31,6 +31,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/apimodule/status" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" @@ -64,7 +65,7 @@ type StatusCreateTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment // module being tested - statusModule *statusModule + statusModule *status.StatusModule } /* @@ -84,7 +85,7 @@ func (suite *StatusCreateTestSuite) SetupSuite() { suite.distributor = testrig.NewTestDistributor() // 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() { @@ -129,7 +130,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["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{ "status": {"this is a brand new status! #helloworld"}, "spoiler_text": {"hello hello"}, @@ -139,7 +140,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { "replyable": {"false"}, "federated": {"false"}, } - suite.statusModule.statusCreatePOSTHandler(ctx) + suite.statusModule.StatusCreatePOSTHandler(ctx) // check response @@ -183,11 +184,11 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["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{ "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) @@ -224,13 +225,13 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["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{ "status": {"this is a reply to a status that doesn't exist"}, "spoiler_text": {"don't open cuz it won't work"}, "in_reply_to_id": {"3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50"}, } - suite.statusModule.statusCreatePOSTHandler(ctx) + suite.statusModule.StatusCreatePOSTHandler(ctx) // check response @@ -255,12 +256,12 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["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{ "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}, } - suite.statusModule.statusCreatePOSTHandler(ctx) + suite.statusModule.StatusCreatePOSTHandler(ctx) // check response suite.EqualValues(http.StatusOK, recorder.Code) @@ -295,12 +296,12 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { ctx.Set(oauth.SessionAuthorizedToken, oauthToken) ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["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{ "status": {"here's an image attachment"}, "media_ids": {"7a3b9f77-ab30-461e-bdd8-e64bd1db3008"}, } - suite.statusModule.statusCreatePOSTHandler(ctx) + suite.statusModule.StatusCreatePOSTHandler(ctx) // check response suite.EqualValues(http.StatusOK, recorder.Code) diff --git a/internal/apimodule/status/statusget_test.go b/internal/apimodule/status/test/statusget_test.go similarity index 95% rename from internal/apimodule/status/statusget_test.go rename to internal/apimodule/status/test/statusget_test.go index 4e25dfbcc..2c2e98acd 100644 --- a/internal/apimodule/status/statusget_test.go +++ b/internal/apimodule/status/test/statusget_test.go @@ -23,6 +23,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/apimodule/status" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" @@ -55,7 +56,7 @@ type StatusGetTestSuite struct { testAttachments map[string]*gtsmodel.MediaAttachment // module being tested - statusModule *statusModule + statusModule *status.StatusModule } /* @@ -75,7 +76,7 @@ func (suite *StatusGetTestSuite) SetupSuite() { suite.distributor = testrig.NewTestDistributor() // 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() { diff --git a/internal/db/pg.go b/internal/db/pg.go index 4098a640b..f2caa813d 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -60,12 +60,6 @@ func newPostgresService(ctx context.Context, c *config.Config, log *logrus.Entry } 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 pgCtx, cancel := context.WithCancel(ctx) 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 - // and listening, and also trigger the opts.OnConnect function passed in above + // actually *begin* the connection so that we can tell if the db is there and listening if err := conn.Ping(ctx); err != nil { cancel() 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) - // 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{ config: c, 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. // In this case, we can still serve the status if it's public, otherwise we definitely shouldn't. if requestingAccount == nil { + if targetStatus.Visibility == gtsmodel.VisibilityPublic { return true, nil } + l.Debug("requesting account is nil but the target status isn't public") return false, nil } // 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. if !requestingAccount.SuspendedAt.IsZero() { + l.Debug("requesting account is suspended") 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 the requesting account is local but doesn't have a corresponding user in the db this is a problem if err == pg.ErrNoRows { + l.Debug("requesting account is local but there's no corresponding user") return false, nil } else { + l.Debugf("requesting account is local but there was an error getting the corresponding user: %s", err) return false, err } } // okay, user exists, so make sure it has full privileges/is confirmed/approved 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 } } + // 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 // 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 { - // 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 } 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 + l.Debug("a block exists between requesting account and target account") return false, nil } diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 76557ca3d..2f90858b4 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -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) fileServerModule := fileserver.New(c, dbService, storageBackend, 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) apiModules := []apimodule.ClientAPIModule{ diff --git a/internal/mastotypes/converter.go b/internal/mastotypes/converter.go index c6516422c..46e7796e7 100644 --- a/internal/mastotypes/converter.go +++ b/internal/mastotypes/converter.go @@ -345,40 +345,53 @@ func (c *converter) StatusToMasto( return nil, fmt.Errorf("error counting faves: %s", err) } - 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) - } + var faved bool + var reblogged bool + var bookmarked bool + var pinned bool + var muted bool - reblogged, err := c.db.StatusRebloggedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) - } + // requestingAccount will be nil for public requests without auth + // But if it's not nil, we can also get information about the requestingAccount's interaction with this status + 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) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err) - } + reblogged, err = c.db.StatusRebloggedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has reblogged status: %s", err) + } - bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID) - if err != nil { - return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err) - } + muted, err = c.db.StatusMutedBy(s, requestingAccount.ID) + if err != nil { + return nil, fmt.Errorf("error checking if requesting account has muted 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) + bookmarked, err = c.db.StatusBookmarkedBy(s, requestingAccount.ID) + if err != nil { + 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 - application := >smodel.Application{} - if err := c.db.GetByID(s.CreatedWithApplicationID, application); err != nil { - return nil, fmt.Errorf("error fetching application used to create status: %s", err) - } - mastoApplication, err := c.AppToMastoPublic(application) - if err != nil { - return nil, fmt.Errorf("error parsing application used to create status: %s", err) + var mastoApplication *mastotypes.Application + if s.CreatedWithApplicationID != "" { + gtsApplication := >smodel.Application{} + if err := c.db.GetByID(s.CreatedWithApplicationID, gtsApplication); err != nil { + return nil, fmt.Errorf("error fetching 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) diff --git a/testrig/actions.go b/testrig/actions.go new file mode 100644 index 000000000..1caa18581 --- /dev/null +++ b/testrig/actions.go @@ -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 . +*/ + +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 +} diff --git a/testrig/router.go b/testrig/router.go new file mode 100644 index 000000000..abd168724 --- /dev/null +++ b/testrig/router.go @@ -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 . +*/ + +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 +} diff --git a/testrig/testmodels.go b/testrig/testmodels.go index cbc21237b..829c40455 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -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. func NewTestClients() 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": { ID: "73b48d42-029d-4487-80fc-329a5cf67869", Secret: "c3724c74-dc3b-41b2-a108-0ea3d8399830", Domain: "http://localhost:8080", 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 } @@ -61,6 +73,16 @@ func NewTestClients() map[string]*oauth.Client { // NewTestApplications returns a map of applications keyed to which number application they are. func NewTestApplications() 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": { ID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", Name: "really cool gts application", @@ -71,6 +93,16 @@ func NewTestApplications() map[string]*gtsmodel.Application { Scopes: "read write follow push", 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 } @@ -128,7 +160,7 @@ func NewTestUsers() map[string]*gtsmodel.User { CreatedByApplicationID: "", LastEmailedAt: time.Now().Add(-30 * time.Minute), ConfirmationToken: "", - ConfirmedAt: time.Time{}, + ConfirmedAt: time.Now().Add(-72 * time.Hour), ConfirmationSentAt: time.Time{}, UnconfirmedEmail: "", Moderator: true, @@ -689,13 +721,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedAt: time.Now().Add(-71 * time.Hour), UpdatedAt: time.Now().Add(-71 * time.Hour), Local: true, - AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", + AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", InReplyToID: "", BoostOfID: "", ContentWarning: "", Visibility: gtsmodel.VisibilityPublic, Sensitive: false, Language: "en", + CreatedWithApplicationID: "9bf9e368-037f-444d-8ffd-1091d1c21c4c", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, @@ -712,13 +745,14 @@ func NewTestStatuses() map[string]*gtsmodel.Status { CreatedAt: time.Now().Add(-70 * time.Hour), UpdatedAt: time.Now().Add(-70 * time.Hour), Local: true, - AccountID: "0fb02eae-2214-473f-9667-0a43f22d75ff", + AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", InReplyToID: "", BoostOfID: "", ContentWarning: "open to see some puppies", Visibility: gtsmodel.VisibilityPublic, Sensitive: true, Language: "en", + CreatedWithApplicationID: "9bf9e368-037f-444d-8ffd-1091d1c21c4c", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, @@ -742,6 +776,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityPublic, Sensitive: true, Language: "en", + CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, @@ -765,6 +800,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityUnlocked, Sensitive: false, Language: "en", + CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: false, Boostable: true, @@ -788,6 +824,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityMutualsOnly, Sensitive: false, Language: "en", + CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: false, @@ -812,6 +849,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityMutualsOnly, Sensitive: false, Language: "en", + CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, @@ -835,6 +873,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityPublic, Sensitive: true, Language: "en", + CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true, @@ -858,6 +897,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status { Visibility: gtsmodel.VisibilityPublic, Sensitive: true, Language: "en", + CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, Boostable: true,