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

@ -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{}{
&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".
// 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,

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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),
},
}

View file

@ -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{}{
&gtsmodel.MediaAttachment{},
}

View file

@ -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 {

View file

@ -16,7 +16,7 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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()

View file

@ -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{}{
&gtsmodel.User{},
&gtsmodel.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)
}
}

View file

@ -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 {

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"
)
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

View file

@ -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)

View file

@ -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() {

View file

@ -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
}

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)
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{

View file

@ -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 := &gtsmodel.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 := &gtsmodel.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)