diff --git a/PROGRESS.md b/PROGRESS.md index 013ad080a..18bcedfa3 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -74,9 +74,9 @@ * [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) - * [ ] /api/v1/statuses/:id/favourite POST (Fave a status) - * [ ] /api/v1/statuses/:id/favourite POST (Unfave a status) + * [x] /api/v1/statuses/:id/favourited_by GET (See who has faved a status) + * [x] /api/v1/statuses/:id/favourite POST (Fave a status) + * [x] /api/v1/statuses/:id/unfavourite POST (Unfave a status) * [ ] /api/v1/statuses/:id/reblog POST (Reblog a status) * [ ] /api/v1/statuses/:id/unreblog POST (Undo a reblog) * [ ] /api/v1/statuses/:id/bookmark POST (Bookmark a status) diff --git a/internal/apimodule/account/account.go b/internal/apimodule/account/account.go index a94169eb2..f4a47f6a2 100644 --- a/internal/apimodule/account/account.go +++ b/internal/apimodule/account/account.go @@ -69,6 +69,7 @@ func New(config *config.Config, db db.DB, oauthServer oauth.Server, mediaHandler func (m *accountModule) Route(r router.Router) error { r.AttachHandler(http.MethodPost, basePath, m.accountCreatePOSTHandler) r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler) + r.AttachHandler(http.MethodPatch, basePathWithID, m.muxHandler) return nil } @@ -94,11 +95,16 @@ func (m *accountModule) CreateTables(db db.DB) error { func (m *accountModule) muxHandler(c *gin.Context) { ru := c.Request.RequestURI - if strings.HasPrefix(ru, verifyPath) { - m.accountVerifyGETHandler(c) - } else if strings.HasPrefix(ru, updateCredentialsPath) { - m.accountUpdateCredentialsPATCHHandler(c) - } else { - m.accountGETHandler(c) + switch c.Request.Method { + case http.MethodGet: + if strings.HasPrefix(ru, verifyPath) { + m.accountVerifyGETHandler(c) + } else { + m.accountGETHandler(c) + } + case http.MethodPatch: + if strings.HasPrefix(ru, updateCredentialsPath) { + m.accountUpdateCredentialsPATCHHandler(c) + } } } diff --git a/internal/apimodule/fileserver/servefile.go b/internal/apimodule/fileserver/servefile.go index 573ad0694..0421c5095 100644 --- a/internal/apimodule/fileserver/servefile.go +++ b/internal/apimodule/fileserver/servefile.go @@ -156,6 +156,8 @@ func (m *FileServer) serveAttachment(c *gin.Context, accountID string, mediaType return } + l.Errorf("about to serve content length: %d attachment bytes is: %d", int64(contentLength), int64(len(attachmentBytes))) + // finally we can return with all the information we derived above c.DataFromReader(http.StatusOK, int64(contentLength), contentType, bytes.NewReader(attachmentBytes), map[string]string{}) } diff --git a/internal/apimodule/status/status.go b/internal/apimodule/status/status.go index 37e6ee268..2ecd009bf 100644 --- a/internal/apimodule/status/status.go +++ b/internal/apimodule/status/status.go @@ -36,21 +36,28 @@ import ( ) 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 = BasePathWithID + "/context" + + FavouritedPath = BasePathWithID + "/favourited_by" + FavouritePath = BasePathWithID + "/favourite" + UnfavouritePath = BasePathWithID + "/unfavourite" + + RebloggedPath = BasePathWithID + "/reblogged_by" + ReblogPath = BasePathWithID + "/reblog" + UnreblogPath = BasePathWithID + "/unreblog" + + BookmarkPath = BasePathWithID + "/bookmark" + UnbookmarkPath = BasePathWithID + "/unbookmark" + + MutePath = BasePathWithID + "/mute" + UnmutePath = BasePathWithID + "/unmute" + + PinPath = BasePathWithID + "/pin" + UnpinPath = BasePathWithID + "/unpin" ) type StatusModule struct { @@ -77,8 +84,13 @@ func New(config *config.Config, db db.DB, mediaHandler media.MediaHandler, masto // 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.MethodDelete, BasePathWithID, m.StatusDELETEHandler) + + r.AttachHandler(http.MethodPost, FavouritePath, m.StatusFavePOSTHandler) + r.AttachHandler(http.MethodPost, UnfavouritePath, m.StatusFavePOSTHandler) + + r.AttachHandler(http.MethodGet, BasePathWithID, m.muxHandler) - r.AttachHandler(http.MethodDelete, BasePathWithID, m.muxHandler) return nil } @@ -113,16 +125,15 @@ func (m *StatusModule) CreateTables(db db.DB) error { func (m *StatusModule) muxHandler(c *gin.Context) { m.log.Debug("entering mux handler") ru := c.Request.RequestURI - if c.Request.Method == http.MethodGet { + + switch c.Request.Method { + case http.MethodGet: if strings.HasPrefix(ru, ContextPath) { // TODO - } else if strings.HasPrefix(ru, RebloggedPath) { - // TODO + } else if strings.HasPrefix(ru, FavouritedPath) { + m.StatusFavedByGETHandler(c) } else { m.StatusGETHandler(c) } } - if c.Request.Method == http.MethodDelete { - m.StatusDELETEHandler(c) - } } diff --git a/internal/apimodule/status/statusfave.go b/internal/apimodule/status/statusfave.go new file mode 100644 index 000000000..de475b905 --- /dev/null +++ b/internal/apimodule/status/statusfave.go @@ -0,0 +1,136 @@ +/* + 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) StatusFavePOSTHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "StatusFavePOSTHandler", + "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 fave 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 + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + 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 + } + + l.Trace("going to see if status is visible") + visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + if err != nil { + l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + if !visible { + l.Trace("status is not visible") + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + // is the status faveable? + if !targetStatus.VisibilityAdvanced.Likeable { + l.Debug("status is not faveable") + c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable", targetStatusID)}) + return + } + + // it's visible! it's faveable! so let's fave the FUCK out of it + fave, err := m.db.FaveStatus(targetStatus, authed.Account.ID) + if err != nil { + l.Debugf("error faveing status: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + 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, targetAccount, 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 the targeted status was already faved, faved will be nil + // only put the fave in the distributor if something actually changed + if fave != nil { + fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor + m.distributor.FromClientAPI() <- distributor.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsNote, // status is a note + APActivityType: gtsmodel.ActivityStreamsLike, // we're creating a like/fave on the note + Activity: fave, // pass the fave along for processing + } + } + + c.JSON(http.StatusOK, mastoStatus) +} diff --git a/internal/apimodule/status/statusfavedby.go b/internal/apimodule/status/statusfavedby.go new file mode 100644 index 000000000..df08ad814 --- /dev/null +++ b/internal/apimodule/status/statusfavedby.go @@ -0,0 +1,128 @@ +/* + 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" + mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +func (m *StatusModule) StatusFavedByGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "statusGETHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + l.Debugf("entering function") + + var requestingAccount *gtsmodel.Account + 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 but will continue to serve anyway if public status") + requestingAccount = nil + } else { + requestingAccount = authed.Account + } + + 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 + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + 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 + } + + l.Trace("going to see if status is visible") + visible, err := m.db.StatusVisible(targetStatus, targetAccount, requestingAccount, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + if err != nil { + l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + if !visible { + l.Trace("status is not visible") + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + // get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff + favingAccounts, err := m.db.WhoFavedStatus(targetStatus) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error":err.Error()}) + return + } + + // filter the list so the user doesn't see accounts they blocked or which blocked them + filteredAccounts := []*gtsmodel.Account{} + for _, acc := range favingAccounts { + blocked, err := m.db.Blocked(authed.Account.ID, acc.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error":err.Error()}) + return + } + if !blocked { + filteredAccounts = append(filteredAccounts, acc) + } + } + + // TODO: filter other things here? suspended? muted? silenced? + + // now we can return the masto representation of those accounts + mastoAccounts := []*mastotypes.Account{} + for _, acc := range filteredAccounts { + mastoAccount, err := m.mastoConverter.AccountToMastoPublic(acc) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error":err.Error()}) + return + } + mastoAccounts = append(mastoAccounts, mastoAccount) + } + + c.JSON(http.StatusOK, mastoAccounts) +} diff --git a/internal/apimodule/status/statusunfave.go b/internal/apimodule/status/statusunfave.go new file mode 100644 index 000000000..61ffd8e4c --- /dev/null +++ b/internal/apimodule/status/statusunfave.go @@ -0,0 +1,136 @@ +/* + 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) StatusUnfavePOSTHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "StatusUnfavePOSTHandler", + "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 unfave 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 + } + + l.Tracef("going to search for target account %s", targetStatus.AccountID) + targetAccount := >smodel.Account{} + if err := m.db.GetByID(targetStatus.AccountID, targetAccount); err != nil { + l.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + 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 + } + + l.Trace("going to see if status is visible") + visible, err := m.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that + if err != nil { + l.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err) + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + if !visible { + l.Trace("status is not visible") + c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)}) + return + } + + // is the status faveable? + if !targetStatus.VisibilityAdvanced.Likeable { + l.Debug("status is not faveable") + c.JSON(http.StatusForbidden, gin.H{"error": fmt.Sprintf("status %s not faveable so therefore not unfave-able", targetStatusID)}) + return + } + + // it's visible! it's faveable! so let's unfave the FUCK out of it + fave, err := m.db.UnfaveStatus(targetStatus, authed.Account.ID) + if err != nil { + l.Debugf("error unfaveing status: %s", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + 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, targetAccount, 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 + } + + // fave might be nil if this status wasn't faved in the first place + // we only want to pass the message to the distributor if something actually changed + if fave != nil { + fave.FavedStatus = targetStatus // attach the status pointer to the fave for easy retrieval in the distributor + m.distributor.FromClientAPI() <- distributor.FromClientAPI{ + APObjectType: gtsmodel.ActivityStreamsNote, // status is a note + APActivityType: gtsmodel.ActivityStreamsUndo, // undo the fave + Activity: fave, // pass the undone fave along + } + } + + c.JSON(http.StatusOK, mastoStatus) +} diff --git a/internal/apimodule/status/test/statusfave_test.go b/internal/apimodule/status/test/statusfave_test.go new file mode 100644 index 000000000..aa2a2ecb3 --- /dev/null +++ b/internal/apimodule/status/test/statusfave_test.go @@ -0,0 +1,208 @@ +/* + 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "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" + "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/storage" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type StatusFaveTestSuite struct { + // standard suite interfaces + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + storage storage.Storage + mastoConverter mastotypes.Converter + mediaHandler media.MediaHandler + oauthServer oauth.Server + distributor distributor.Distributor + + // standard suite models + testTokens map[string]*oauth.Token + testClients map[string]*oauth.Client + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + testAttachments map[string]*gtsmodel.MediaAttachment + testStatuses map[string]*gtsmodel.Status + + // module being tested + statusModule *status.StatusModule +} + +/* + TEST INFRASTRUCTURE +*/ + +// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout +func (suite *StatusFaveTestSuite) SetupSuite() { + // setup standard items + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + suite.storage = testrig.NewTestStorage() + suite.mastoConverter = testrig.NewTestMastoConverter(suite.db) + suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) + suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.distributor = testrig.NewTestDistributor() + + // setup module being tested + suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) +} + +func (suite *StatusFaveTestSuite) TearDownSuite() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +func (suite *StatusFaveTestSuite) SetupTest() { + testrig.StandardDBSetup(suite.db) + testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() +} + +// TearDownTest drops tables to make sure there's no data in the db +func (suite *StatusFaveTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +/* + ACTUAL TESTS +*/ + +// fave a status +func (suite *StatusFaveTestSuite) TestPostFave() { + + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + + targetStatus := suite.testStatuses["admin_account_status_2"] + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + 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", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: status.IDKey, + Value: targetStatus.ID, + }, + } + + suite.statusModule.StatusFavePOSTHandler(ctx) + + // check response + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + statusReply := &mastomodel.Status{} + err = json.Unmarshal(b, statusReply) + assert.NoError(suite.T(), err) + + assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) + assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) + assert.True(suite.T(), statusReply.Sensitive) + assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility) + assert.True(suite.T(), statusReply.Favourited) + assert.Equal(suite.T(), 1, statusReply.FavouritesCount) +} + +// try to fave a status that's not faveable +func (suite *StatusFaveTestSuite) TestPostUnfaveable() { + + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + targetStatus := suite.testStatuses["local_account_2_status_3"] // this one is unlikeable and unreplyable + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + 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", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: status.IDKey, + Value: targetStatus.ID, + }, + } + + suite.statusModule.StatusFavePOSTHandler(ctx) + + // check response + suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unlikeable statuses + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + assert.Equal(suite.T(), fmt.Sprintf(`{"error":"status %s not faveable"}`, targetStatus.ID), string(b)) +} + +func TestStatusFaveTestSuite(t *testing.T) { + suite.Run(t, new(StatusFaveTestSuite)) +} diff --git a/internal/apimodule/status/test/statusfavedby_test.go b/internal/apimodule/status/test/statusfavedby_test.go new file mode 100644 index 000000000..83f66562b --- /dev/null +++ b/internal/apimodule/status/test/statusfavedby_test.go @@ -0,0 +1,159 @@ +/* + 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "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" + "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/storage" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type StatusFavedByTestSuite struct { + // standard suite interfaces + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + storage storage.Storage + mastoConverter mastotypes.Converter + mediaHandler media.MediaHandler + oauthServer oauth.Server + distributor distributor.Distributor + + // standard suite models + testTokens map[string]*oauth.Token + testClients map[string]*oauth.Client + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + testAttachments map[string]*gtsmodel.MediaAttachment + testStatuses map[string]*gtsmodel.Status + + // module being tested + statusModule *status.StatusModule +} + +// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout +func (suite *StatusFavedByTestSuite) SetupSuite() { + // setup standard items + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + suite.storage = testrig.NewTestStorage() + suite.mastoConverter = testrig.NewTestMastoConverter(suite.db) + suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) + suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.distributor = testrig.NewTestDistributor() + + // setup module being tested + suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) +} + +func (suite *StatusFavedByTestSuite) TearDownSuite() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +func (suite *StatusFavedByTestSuite) SetupTest() { + testrig.StandardDBSetup(suite.db) + testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() +} + +// TearDownTest drops tables to make sure there's no data in the db +func (suite *StatusFavedByTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +/* + ACTUAL TESTS +*/ + +func (suite *StatusFavedByTestSuite) TestGetFavedBy() { + t := suite.testTokens["local_account_2"] + oauthToken := oauth.PGTokenToOauthToken(t) + + targetStatus := suite.testStatuses["admin_account_status_1"] // this status is faved by local_account_1 + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_2"]) + ctx.Set(oauth.SessionAuthorizedToken, oauthToken) + ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) + ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) + ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: status.IDKey, + Value: targetStatus.ID, + }, + } + + suite.statusModule.StatusFavedByGETHandler(ctx) + + // check response + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + accts := []mastomodel.Account{} + err = json.Unmarshal(b, &accts) + assert.NoError(suite.T(), err) + + assert.Len(suite.T(), accts, 1) + assert.Equal(suite.T(), "the_mighty_zork", accts[0].Username) +} + +func TestStatusFavedByTestSuite(t *testing.T) { + suite.Run(t, new(StatusFavedByTestSuite)) +} diff --git a/internal/apimodule/status/test/statusunfave_test.go b/internal/apimodule/status/test/statusunfave_test.go new file mode 100644 index 000000000..81276a1ed --- /dev/null +++ b/internal/apimodule/status/test/statusunfave_test.go @@ -0,0 +1,219 @@ +/* + 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/gin-gonic/gin" + "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" + "github.com/superseriousbusiness/gotosocial/internal/distributor" + "github.com/superseriousbusiness/gotosocial/internal/mastotypes" + mastomodel "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/storage" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type StatusUnfaveTestSuite struct { + // standard suite interfaces + suite.Suite + config *config.Config + db db.DB + log *logrus.Logger + storage storage.Storage + mastoConverter mastotypes.Converter + mediaHandler media.MediaHandler + oauthServer oauth.Server + distributor distributor.Distributor + + // standard suite models + testTokens map[string]*oauth.Token + testClients map[string]*oauth.Client + testApplications map[string]*gtsmodel.Application + testUsers map[string]*gtsmodel.User + testAccounts map[string]*gtsmodel.Account + testAttachments map[string]*gtsmodel.MediaAttachment + testStatuses map[string]*gtsmodel.Status + + // module being tested + statusModule *status.StatusModule +} + +/* + TEST INFRASTRUCTURE +*/ + +// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout +func (suite *StatusUnfaveTestSuite) SetupSuite() { + // setup standard items + suite.config = testrig.NewTestConfig() + suite.db = testrig.NewTestDB() + suite.log = testrig.NewTestLog() + suite.storage = testrig.NewTestStorage() + suite.mastoConverter = testrig.NewTestMastoConverter(suite.db) + suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) + suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.distributor = testrig.NewTestDistributor() + + // setup module being tested + suite.statusModule = status.New(suite.config, suite.db, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*status.StatusModule) +} + +func (suite *StatusUnfaveTestSuite) TearDownSuite() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +func (suite *StatusUnfaveTestSuite) SetupTest() { + testrig.StandardDBSetup(suite.db) + testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") + suite.testTokens = testrig.NewTestTokens() + suite.testClients = testrig.NewTestClients() + suite.testApplications = testrig.NewTestApplications() + suite.testUsers = testrig.NewTestUsers() + suite.testAccounts = testrig.NewTestAccounts() + suite.testAttachments = testrig.NewTestAttachments() + suite.testStatuses = testrig.NewTestStatuses() +} + +// TearDownTest drops tables to make sure there's no data in the db +func (suite *StatusUnfaveTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) + testrig.StandardStorageTeardown(suite.storage) +} + +/* + ACTUAL TESTS +*/ + +// unfave a status +func (suite *StatusUnfaveTestSuite) TestPostUnfave() { + + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + // this is the status we wanna unfave: in the testrig it's already faved by this account + targetStatus := suite.testStatuses["admin_account_status_1"] + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + 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", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: status.IDKey, + Value: targetStatus.ID, + }, + } + + suite.statusModule.StatusUnfavePOSTHandler(ctx) + + // check response + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + statusReply := &mastomodel.Status{} + err = json.Unmarshal(b, statusReply) + assert.NoError(suite.T(), err) + + assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) + assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) + assert.False(suite.T(), statusReply.Sensitive) + assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility) + assert.False(suite.T(), statusReply.Favourited) + assert.Equal(suite.T(), 0, statusReply.FavouritesCount) +} + +// try to unfave a status that's already not faved +func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { + + t := suite.testTokens["local_account_1"] + oauthToken := oauth.PGTokenToOauthToken(t) + + // this is the status we wanna unfave: in the testrig it's not faved by this account + targetStatus := suite.testStatuses["admin_account_status_2"] + + // setup + recorder := httptest.NewRecorder() + ctx, _ := gin.CreateTestContext(recorder) + ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"]) + 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", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + + // normally the router would populate these params from the path values, + // but because we're calling the function directly, we need to set them manually. + ctx.Params = gin.Params{ + gin.Param{ + Key: status.IDKey, + Value: targetStatus.ID, + }, + } + + suite.statusModule.StatusUnfavePOSTHandler(ctx) + + // check response + suite.EqualValues(http.StatusOK, recorder.Code) + + result := recorder.Result() + defer result.Body.Close() + b, err := ioutil.ReadAll(result.Body) + assert.NoError(suite.T(), err) + + statusReply := &mastomodel.Status{} + err = json.Unmarshal(b, statusReply) + assert.NoError(suite.T(), err) + + assert.Equal(suite.T(), targetStatus.ContentWarning, statusReply.SpoilerText) + assert.Equal(suite.T(), targetStatus.Content, statusReply.Content) + assert.True(suite.T(), statusReply.Sensitive) + assert.Equal(suite.T(), mastomodel.VisibilityPublic, statusReply.Visibility) + assert.False(suite.T(), statusReply.Favourited) + assert.Equal(suite.T(), 0, statusReply.FavouritesCount) +} + +func TestStatusUnfaveTestSuite(t *testing.T) { + suite.Run(t, new(StatusUnfaveTestSuite)) +} diff --git a/internal/db/db.go b/internal/db/db.go index 106780f5d..69ad7b822 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -235,6 +235,18 @@ type DB interface { // StatusPinnedBy checks if a given status has been pinned by a given account ID StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) + // FaveStatus faves the given status, using accountID as the faver. + // The returned fave will be nil if the status was already faved. + FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) + + // UnfaveStatus unfaves the given status, using accountID as the unfaver (sure, that's a word). + // The returned fave will be nil if the status was already not faved. + UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) + + // WhoFavedStatus returns a slice of accounts who faved the given status. + // This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user. + WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) + /* USEFUL CONVERSION FUNCTIONS */ diff --git a/internal/db/gtsmodel/account.go b/internal/db/gtsmodel/account.go index 82f5b5c6f..4bf5a9d33 100644 --- a/internal/db/gtsmodel/account.go +++ b/internal/db/gtsmodel/account.go @@ -44,26 +44,10 @@ type Account struct { ACCOUNT METADATA */ - // File name of the avatar on local storage - AvatarFileName string - // Gif? png? jpeg? - AvatarContentType string - // Size of the avatar in bytes - AvatarFileSize int - // When was the avatar last updated? - AvatarUpdatedAt time.Time `pg:"type:timestamp"` - // Where can the avatar be retrieved? - AvatarRemoteURL string - // File name of the header on local storage - HeaderFileName string - // Gif? png? jpeg? - HeaderContentType string - // Size of the header in bytes - HeaderFileSize int - // When was the header last updated? - HeaderUpdatedAt time.Time `pg:"type:timestamp"` - // Where can the header be retrieved? - HeaderRemoteURL string + // ID of the avatar as a media attachment + AvatarMediaAttachmentID string + // ID of the header as a media attachment + HeaderMediaAttachmentID string // DisplayName for this account. Can be empty, then just the Username will be used for display purposes. DisplayName string // a key/value map of fields that this account has added to their profile diff --git a/internal/db/gtsmodel/statusfave.go b/internal/db/gtsmodel/statusfave.go index 852998387..9fb92b931 100644 --- a/internal/db/gtsmodel/statusfave.go +++ b/internal/db/gtsmodel/statusfave.go @@ -23,13 +23,16 @@ import "time" // StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account type StatusFave struct { // id of this fave in the database - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` // when was this fave created - CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` + CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the fave - AccountID string `pg:",notnull"` + AccountID string `pg:",notnull"` // id the account owning the faved status - TargetAccountID string `pg:",notnull"` + TargetAccountID string `pg:",notnull"` // database id of the status that has been 'faved' - StatusID string `pg:",notnull"` + StatusID string `pg:",notnull"` + + // FavedStatus is the status being interacted with. It won't be put or retrieved from the db, it's just for conveniently passing a pointer around. + FavedStatus *Status `pg:"-"` } diff --git a/internal/db/pg.go b/internal/db/pg.go index f2caa813d..610061324 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -497,12 +497,44 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr } func (ps *postgresService) SetHeaderOrAvatarForAccountID(mediaAttachment *gtsmodel.MediaAttachment, accountID string) error { - _, err := ps.conn.Model(mediaAttachment).Insert() - return err + if mediaAttachment.Avatar && mediaAttachment.Header { + return errors.New("one media attachment cannot be both header and avatar") + } + + var headerOrAVI string + if mediaAttachment.Avatar { + headerOrAVI = "avatar" + } else if mediaAttachment.Header { + headerOrAVI = "header" + } else { + return errors.New("given media attachment was neither a header nor an avatar") + } + + // TODO: there are probably more side effects here that need to be handled + if _, err := ps.conn.Model(mediaAttachment).OnConflict("(id) DO UPDATE").Insert(); err != nil { + return err + } + + if _, err := ps.conn.Model(>smodel.Account{}).Set(fmt.Sprintf("%s_media_attachment_id = ?", headerOrAVI), mediaAttachment.ID).Where("id = ?", accountID).Update(); err != nil { + return err + } + return nil } func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachment, accountID string) error { - if err := ps.conn.Model(header).Where("account_id = ?", accountID).Where("header = ?", true).Select(); err != nil { + acct := >smodel.Account{} + if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return ErrNoEntries{} + } + return err + } + + if acct.HeaderMediaAttachmentID == "" { + return ErrNoEntries{} + } + + if err := ps.conn.Model(header).Where("id = ?", acct.HeaderMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } @@ -512,7 +544,19 @@ func (ps *postgresService) GetHeaderForAccountID(header *gtsmodel.MediaAttachmen } func (ps *postgresService) GetAvatarForAccountID(avatar *gtsmodel.MediaAttachment, accountID string) error { - if err := ps.conn.Model(avatar).Where("account_id = ?", accountID).Where("avatar = ?", true).Select(); err != nil { + acct := >smodel.Account{} + if err := ps.conn.Model(acct).Where("id = ?", accountID).Select(); err != nil { + if err == pg.ErrNoRows { + return ErrNoEntries{} + } + return err + } + + if acct.AvatarMediaAttachmentID == "" { + return ErrNoEntries{} + } + + if err := ps.conn.Model(avatar).Where("id = ?", acct.AvatarMediaAttachmentID).Select(); err != nil { if err == pg.ErrNoRows { return ErrNoEntries{} } @@ -806,6 +850,79 @@ func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID str return ps.conn.Model(>smodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists() } +func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { + // first check if a fave already exists, we can just return if so + existingFave := >smodel.StatusFave{} + err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() + if err == nil { + // fave already exists so just return nothing at all + return nil, nil + } + + // an error occurred so it might exist or not, we don't know + if err != pg.ErrNoRows { + return nil, err + } + + // it doesn't exist so create it + newFave := >smodel.StatusFave{ + AccountID: accountID, + TargetAccountID: status.AccountID, + StatusID: status.ID, + } + if _, err = ps.conn.Model(newFave).Insert(); err != nil { + return nil, err + } + + return newFave, nil +} + +func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) { + // if a fave doesn't exist, we don't need to do anything + existingFave := >smodel.StatusFave{} + err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select() + // the fave doesn't exist so return nothing at all + if err == pg.ErrNoRows { + return nil, nil + } + + // an error occurred so it might exist or not, we don't know + if err != nil && err != pg.ErrNoRows { + return nil, err + } + + // the fave exists so remove it + if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil { + return nil, err + } + + return existingFave, nil +} + +func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) { + accounts := []*gtsmodel.Account{} + + faves := []*gtsmodel.StatusFave{} + if err := ps.conn.Model(&faves).Where("status_id = ?", status.ID).Select(); err != nil { + if err == pg.ErrNoRows { + return accounts, nil // no rows just means nobody has faved this status, so that's fine + } + return nil, err // an actual error has occurred + } + + for _, f := range faves { + acc := >smodel.Account{} + if err := ps.conn.Model(acc).Where("id = ?", f.AccountID).Select(); err != nil { + if err == pg.ErrNoRows { + continue // the account doesn't exist for some reason??? but this isn't the place to worry about that so just skip it + } + return nil, err // an actual error has occurred + } + accounts = append(accounts, acc) + } + return accounts, nil +} + /* CONVERSION FUNCTIONS */ diff --git a/internal/mastotypes/converter.go b/internal/mastotypes/converter.go index 46e7796e7..af78b5f15 100644 --- a/internal/mastotypes/converter.go +++ b/internal/mastotypes/converter.go @@ -170,8 +170,8 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Accou return nil, fmt.Errorf("error getting avatar: %s", err) } } - aviURL := avi.File.Path - aviURLStatic := avi.Thumbnail.Path + aviURL := avi.URL + aviURLStatic := avi.Thumbnail.URL header := >smodel.MediaAttachment{} if err := c.db.GetHeaderForAccountID(avi, a.ID); err != nil { @@ -179,8 +179,8 @@ func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Accou return nil, fmt.Errorf("error getting header: %s", err) } } - headerURL := header.File.Path - headerURLStatic := header.Thumbnail.Path + headerURL := header.URL + headerURLStatic := header.Thumbnail.URL // get the fields set on this account fields := []mastotypes.Field{} diff --git a/testrig/db.go b/testrig/db.go index 6f61b3763..5974eae69 100644 --- a/testrig/db.go +++ b/testrig/db.go @@ -123,6 +123,12 @@ func StandardDBSetup(db db.DB) { } } + for _, v := range NewTestFaves() { + if err := db.Put(v); err != nil { + panic(err) + } + } + if err := db.CreateInstanceAccount(); err != nil { panic(err) } diff --git a/testrig/media/zork-original.jpeg b/testrig/media/zork-original.jpeg new file mode 100644 index 000000000..7d8bc1fc7 Binary files /dev/null and b/testrig/media/zork-original.jpeg differ diff --git a/testrig/media/zork-small.jpeg b/testrig/media/zork-small.jpeg new file mode 100644 index 000000000..60be12564 Binary files /dev/null and b/testrig/media/zork-small.jpeg differ diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 829c40455..f028bbd8d 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -41,6 +41,16 @@ func NewTestTokens() map[string]*oauth.Token { AccessCreateAt: time.Now(), AccessExpiresAt: time.Now().Add(72 * time.Hour), }, + "local_account_2": { + ID: "b04cae99-39b5-4610-a425-dc6b91c78a72", + ClientID: "a4f6a2ea-a32b-4600-8853-72fc4ad98a1f", + UserID: "d120bd97-866f-4a05-9690-a1294b9934c3", + RedirectURI: "http://localhost:8080", + Scope: "read write follow push", + Access: "PIPINALKNNNFNF98717NAMNAMNFKIJKJ881818KJKJAKJJJA", + AccessCreateAt: time.Now(), + AccessExpiresAt: time.Now().Add(72 * time.Hour), + }, } return tokens } @@ -243,184 +253,152 @@ func NewTestAccounts() map[string]*gtsmodel.Account { Username: "localhost:8080", }, "unconfirmed_account": { - ID: "59e197f5-87cd-4be8-ac7c-09082ccc4b4d", - Username: "weed_lord420", - AvatarFileName: "", - AvatarContentType: "", - AvatarFileSize: 0, - AvatarUpdatedAt: time.Time{}, - AvatarRemoteURL: "", - HeaderFileName: "", - HeaderContentType: "", - HeaderFileSize: 0, - HeaderUpdatedAt: time.Time{}, - HeaderRemoteURL: "", - DisplayName: "", - Fields: []gtsmodel.Field{}, - Note: "", - Memorial: false, - MovedToAccountID: "", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Bot: false, - Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.", - Locked: false, - Discoverable: false, - Privacy: gtsmodel.VisibilityPublic, - Sensitive: false, - Language: "en", - URI: "http://localhost:8080/users/weed_lord420", - URL: "http://localhost:8080/@weed_lord420", - LastWebfingeredAt: time.Time{}, - InboxURL: "http://localhost:8080/users/weed_lord420/inbox", - OutboxURL: "http://localhost:8080/users/weed_lord420/outbox", - SharedInboxURL: "", - FollowersURL: "http://localhost:8080/users/weed_lord420/followers", - FeaturedCollectionURL: "http://localhost:8080/users/weed_lord420/collections/featured", - ActorType: gtsmodel.ActivityStreamsPerson, - AlsoKnownAs: "", - PrivateKey: &rsa.PrivateKey{}, - PublicKey: &rsa.PublicKey{}, - SensitizedAt: time.Time{}, - SilencedAt: time.Time{}, - SuspendedAt: time.Time{}, - HideCollections: false, - SuspensionOrigin: "", + ID: "59e197f5-87cd-4be8-ac7c-09082ccc4b4d", + Username: "weed_lord420", + AvatarMediaAttachmentID: "", + HeaderMediaAttachmentID: "", + DisplayName: "", + Fields: []gtsmodel.Field{}, + Note: "", + Memorial: false, + MovedToAccountID: "", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Bot: false, + Reason: "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.", + Locked: false, + Discoverable: false, + Privacy: gtsmodel.VisibilityPublic, + Sensitive: false, + Language: "en", + URI: "http://localhost:8080/users/weed_lord420", + URL: "http://localhost:8080/@weed_lord420", + LastWebfingeredAt: time.Time{}, + InboxURL: "http://localhost:8080/users/weed_lord420/inbox", + OutboxURL: "http://localhost:8080/users/weed_lord420/outbox", + SharedInboxURL: "", + FollowersURL: "http://localhost:8080/users/weed_lord420/followers", + FeaturedCollectionURL: "http://localhost:8080/users/weed_lord420/collections/featured", + ActorType: gtsmodel.ActivityStreamsPerson, + AlsoKnownAs: "", + PrivateKey: &rsa.PrivateKey{}, + PublicKey: &rsa.PublicKey{}, + SensitizedAt: time.Time{}, + SilencedAt: time.Time{}, + SuspendedAt: time.Time{}, + HideCollections: false, + SuspensionOrigin: "", }, "admin_account": { - ID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", - Username: "admin", - AvatarFileName: "", - AvatarContentType: "", - AvatarFileSize: 0, - AvatarUpdatedAt: time.Time{}, - AvatarRemoteURL: "", - HeaderFileName: "", - HeaderContentType: "", - HeaderFileSize: 0, - HeaderUpdatedAt: time.Time{}, - HeaderRemoteURL: "", - DisplayName: "", - Fields: []gtsmodel.Field{}, - Note: "", - Memorial: false, - MovedToAccountID: "", - CreatedAt: time.Now().Add(-72 * time.Hour), - UpdatedAt: time.Now().Add(-72 * time.Hour), - Bot: false, - Reason: "", - Locked: false, - Discoverable: true, - Privacy: gtsmodel.VisibilityPublic, - Sensitive: false, - Language: "en", - URI: "http://localhost:8080/users/admin", - URL: "http://localhost:8080/@admin", - LastWebfingeredAt: time.Time{}, - InboxURL: "http://localhost:8080/users/admin/inbox", - OutboxURL: "http://localhost:8080/users/admin/outbox", - SharedInboxURL: "", - FollowersURL: "http://localhost:8080/users/admin/followers", - FeaturedCollectionURL: "http://localhost:8080/users/admin/collections/featured", - ActorType: gtsmodel.ActivityStreamsPerson, - AlsoKnownAs: "", - PrivateKey: &rsa.PrivateKey{}, - PublicKey: &rsa.PublicKey{}, - SensitizedAt: time.Time{}, - SilencedAt: time.Time{}, - SuspendedAt: time.Time{}, - HideCollections: false, - SuspensionOrigin: "", + ID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", + Username: "admin", + AvatarMediaAttachmentID: "", + HeaderMediaAttachmentID: "", + DisplayName: "", + Fields: []gtsmodel.Field{}, + Note: "", + Memorial: false, + MovedToAccountID: "", + CreatedAt: time.Now().Add(-72 * time.Hour), + UpdatedAt: time.Now().Add(-72 * time.Hour), + Bot: false, + Reason: "", + Locked: false, + Discoverable: true, + Privacy: gtsmodel.VisibilityPublic, + Sensitive: false, + Language: "en", + URI: "http://localhost:8080/users/admin", + URL: "http://localhost:8080/@admin", + LastWebfingeredAt: time.Time{}, + InboxURL: "http://localhost:8080/users/admin/inbox", + OutboxURL: "http://localhost:8080/users/admin/outbox", + SharedInboxURL: "", + FollowersURL: "http://localhost:8080/users/admin/followers", + FeaturedCollectionURL: "http://localhost:8080/users/admin/collections/featured", + ActorType: gtsmodel.ActivityStreamsPerson, + AlsoKnownAs: "", + PrivateKey: &rsa.PrivateKey{}, + PublicKey: &rsa.PublicKey{}, + SensitizedAt: time.Time{}, + SilencedAt: time.Time{}, + SuspendedAt: time.Time{}, + HideCollections: false, + SuspensionOrigin: "", }, "local_account_1": { - ID: "580072df-4d03-4684-a412-89fd6f7d77e6", - Username: "the_mighty_zork", - AvatarFileName: "http://localhost:8080/fileserver/media/580072df-4d03-4684-a412-89fd6f7d77e6/avatar/original/75cfbe52-a5fb-451b-8f5a-b023229dce8d.jpeg", - AvatarContentType: "image/jpeg", - AvatarFileSize: 0, - AvatarUpdatedAt: time.Time{}, - AvatarRemoteURL: "", - HeaderFileName: "http://localhost:8080/fileserver/media/580072df-4d03-4684-a412-89fd6f7d77e6/header/original/9651c1ed-c288-4063-a95c-c7f8ff2a633f.jpeg", - HeaderContentType: "image/jpeg", - HeaderFileSize: 0, - HeaderUpdatedAt: time.Time{}, - HeaderRemoteURL: "", - DisplayName: "original zork (he/they)", - Fields: []gtsmodel.Field{}, - Note: "hey yo this is my profile!", - Memorial: false, - MovedToAccountID: "", - CreatedAt: time.Now().Add(-48 * time.Hour), - UpdatedAt: time.Now().Add(-48 * time.Hour), - Bot: false, - Reason: "I wanna be on this damned webbed site so bad! Please! Wow", - Locked: false, - Discoverable: true, - Privacy: gtsmodel.VisibilityPublic, - Sensitive: false, - Language: "en", - URI: "http://localhost:8080/users/the_mighty_zork", - URL: "http://localhost:8080/@the_mighty_zork", - LastWebfingeredAt: time.Time{}, - InboxURL: "http://localhost:8080/users/the_mighty_zork/inbox", - OutboxURL: "http://localhost:8080/users/the_mighty_zork/outbox", - SharedInboxURL: "", - FollowersURL: "http://localhost:8080/users/the_mighty_zork/followers", - FeaturedCollectionURL: "http://localhost:8080/users/the_mighty_zork/collections/featured", - ActorType: gtsmodel.ActivityStreamsPerson, - AlsoKnownAs: "", - PrivateKey: &rsa.PrivateKey{}, - PublicKey: &rsa.PublicKey{}, - SensitizedAt: time.Time{}, - SilencedAt: time.Time{}, - SuspendedAt: time.Time{}, - HideCollections: false, - SuspensionOrigin: "", + ID: "580072df-4d03-4684-a412-89fd6f7d77e6", + Username: "the_mighty_zork", + AvatarMediaAttachmentID: "a849906f-8b8e-4b43-ac2f-6979ccbcd442", + HeaderMediaAttachmentID: "", + DisplayName: "original zork (he/they)", + Fields: []gtsmodel.Field{}, + Note: "hey yo this is my profile!", + Memorial: false, + MovedToAccountID: "", + CreatedAt: time.Now().Add(-48 * time.Hour), + UpdatedAt: time.Now().Add(-48 * time.Hour), + Bot: false, + Reason: "I wanna be on this damned webbed site so bad! Please! Wow", + Locked: false, + Discoverable: true, + Privacy: gtsmodel.VisibilityPublic, + Sensitive: false, + Language: "en", + URI: "http://localhost:8080/users/the_mighty_zork", + URL: "http://localhost:8080/@the_mighty_zork", + LastWebfingeredAt: time.Time{}, + InboxURL: "http://localhost:8080/users/the_mighty_zork/inbox", + OutboxURL: "http://localhost:8080/users/the_mighty_zork/outbox", + SharedInboxURL: "", + FollowersURL: "http://localhost:8080/users/the_mighty_zork/followers", + FeaturedCollectionURL: "http://localhost:8080/users/the_mighty_zork/collections/featured", + ActorType: gtsmodel.ActivityStreamsPerson, + AlsoKnownAs: "", + PrivateKey: &rsa.PrivateKey{}, + PublicKey: &rsa.PublicKey{}, + SensitizedAt: time.Time{}, + SilencedAt: time.Time{}, + SuspendedAt: time.Time{}, + HideCollections: false, + SuspensionOrigin: "", }, "local_account_2": { - ID: "eecaad73-5703-426d-9312-276641daa31e", - Username: "1happyturtle", - AvatarFileName: "http://localhost:8080/fileserver/media/eecaad73-5703-426d-9312-276641daa31e/avatar/original/d5e7c265-91a6-4d84-8c27-7e1efe5720da.jpeg", - AvatarContentType: "image/jpeg", - AvatarFileSize: 0, - AvatarUpdatedAt: time.Time{}, - AvatarRemoteURL: "", - HeaderFileName: "http://localhost:8080/fileserver/media/eecaad73-5703-426d-9312-276641daa31e/header/original/e75d4117-21b6-4315-a428-eb3944235996.jpeg", - HeaderContentType: "image/jpeg", - HeaderFileSize: 0, - HeaderUpdatedAt: time.Time{}, - HeaderRemoteURL: "", - DisplayName: "happy little turtle :3", - Fields: []gtsmodel.Field{}, - Note: "i post about things that concern me", - Memorial: false, - MovedToAccountID: "", - CreatedAt: time.Now().Add(-190 * time.Hour), - UpdatedAt: time.Now().Add(-36 * time.Hour), - Bot: false, - Reason: "", - Locked: true, - Discoverable: false, - Privacy: gtsmodel.VisibilityFollowersOnly, - Sensitive: false, - Language: "en", - URI: "http://localhost:8080/users/1happyturtle", - URL: "http://localhost:8080/@1happyturtle", - LastWebfingeredAt: time.Time{}, - InboxURL: "http://localhost:8080/users/1happyturtle/inbox", - OutboxURL: "http://localhost:8080/users/1happyturtle/outbox", - SharedInboxURL: "", - FollowersURL: "http://localhost:8080/users/1happyturtle/followers", - FeaturedCollectionURL: "http://localhost:8080/users/1happyturtle/collections/featured", - ActorType: gtsmodel.ActivityStreamsPerson, - AlsoKnownAs: "", - PrivateKey: &rsa.PrivateKey{}, - PublicKey: &rsa.PublicKey{}, - SensitizedAt: time.Time{}, - SilencedAt: time.Time{}, - SuspendedAt: time.Time{}, - HideCollections: false, - SuspensionOrigin: "", + ID: "eecaad73-5703-426d-9312-276641daa31e", + Username: "1happyturtle", + AvatarMediaAttachmentID: "", + HeaderMediaAttachmentID: "", + DisplayName: "happy little turtle :3", + Fields: []gtsmodel.Field{}, + Note: "i post about things that concern me", + Memorial: false, + MovedToAccountID: "", + CreatedAt: time.Now().Add(-190 * time.Hour), + UpdatedAt: time.Now().Add(-36 * time.Hour), + Bot: false, + Reason: "", + Locked: true, + Discoverable: false, + Privacy: gtsmodel.VisibilityFollowersOnly, + Sensitive: false, + Language: "en", + URI: "http://localhost:8080/users/1happyturtle", + URL: "http://localhost:8080/@1happyturtle", + LastWebfingeredAt: time.Time{}, + InboxURL: "http://localhost:8080/users/1happyturtle/inbox", + OutboxURL: "http://localhost:8080/users/1happyturtle/outbox", + SharedInboxURL: "", + FollowersURL: "http://localhost:8080/users/1happyturtle/followers", + FeaturedCollectionURL: "http://localhost:8080/users/1happyturtle/collections/featured", + ActorType: gtsmodel.ActivityStreamsPerson, + AlsoKnownAs: "", + PrivateKey: &rsa.PrivateKey{}, + PublicKey: &rsa.PublicKey{}, + SensitizedAt: time.Time{}, + SilencedAt: time.Time{}, + SuspendedAt: time.Time{}, + HideCollections: false, + SuspensionOrigin: "", }, "remote_account_1": { ID: "c2c6e647-e2a9-4286-883b-e4a188186664", @@ -643,9 +621,58 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { Avatar: false, Header: false, }, + "local_account_1_avatar": { + ID: "a849906f-8b8e-4b43-ac2f-6979ccbcd442", + StatusID: "", // this attachment isn't connected to a status + URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/avatar/original/a849906f-8b8e-4b43-ac2f-6979ccbcd442.jpeg", + RemoteURL: "", + CreatedAt: time.Now().Add(47 * time.Hour), + UpdatedAt: time.Now().Add(47 * time.Hour), + Type: gtsmodel.FileTypeImage, + FileMeta: gtsmodel.FileMeta{ + Original: gtsmodel.Original{ + Width: 1092, + Height: 1800, + Size: 1965600, + Aspect: 0.6066666666666667, + }, + Small: gtsmodel.Small{ + Width: 155, + Height: 256, + Size: 39680, + Aspect: 0.60546875, + }, + Focus: gtsmodel.Focus{ + X: 0, + Y: 0, + }, + }, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + Description: "a green goblin looking nasty", + ScheduledStatusID: "", + Blurhash: "LKK9MT,p|YSNDkJ-5rsmvnwcOoe:", + Processing: 2, + File: gtsmodel.File{ + Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/avatar/original/a849906f-8b8e-4b43-ac2f-6979ccbcd442.jpeg", + ContentType: "image/jpeg", + FileSize: 457680, + UpdatedAt: time.Now().Add(47 * time.Hour), + }, + Thumbnail: gtsmodel.Thumbnail{ + Path: "/gotosocial/storage/580072df-4d03-4684-a412-89fd6f7d77e6/avatar/small/a849906f-8b8e-4b43-ac2f-6979ccbcd442.jpeg", + ContentType: "image/jpeg", + FileSize: 15374, + UpdatedAt: time.Now().Add(47 * time.Hour), + URL: "http://localhost:8080/fileserver/580072df-4d03-4684-a412-89fd6f7d77e6/avatar/small/a849906f-8b8e-4b43-ac2f-6979ccbcd442.jpeg", + RemoteURL: "", + }, + Avatar: true, + Header: false, + }, } } +// NewTestEmojis returns a map of gts emojis, keyed by the emoji shortcode func NewTestEmojis() map[string]*gtsmodel.Emoji { return map[string]*gtsmodel.Emoji{ "rainbow": { @@ -693,9 +720,14 @@ func NewTestStoredAttachments() map[string]filenames { original: "ohyou-original.jpeg", small: "ohyou-small.jpeg", }, + "local_account_1_avatar": { + original: "zork-original.jpeg", + small: "zork-small.jpeg", + }, } } +// NewtestStoredEmoji returns a map of filenames, keyed according to which emoji they pertain to func NewTestStoredEmoji() map[string]filenames { return map[string]filenames{ "rainbow": { @@ -710,24 +742,24 @@ func NewTestStoredEmoji() map[string]filenames { func NewTestStatuses() map[string]*gtsmodel.Status { return map[string]*gtsmodel.Status{ "admin_account_status_1": { - ID: "502ccd6f-0edf-48d7-9016-2dfa4d3714cd", - URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", - URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", - Content: "hello world! #welcome ! first post on the instance :rainbow: !", - Attachments: []string{"b052241b-f30f-4dc6-92fc-2bad0be1f8d8"}, - Tags: []string{"a7e8f5ca-88a1-4652-8079-a187eab8d56e"}, - Mentions: []string{}, - Emojis: []string{"a96ec4f3-1cae-47e4-a508-f9d66a6b221b"}, - CreatedAt: time.Now().Add(-71 * time.Hour), - UpdatedAt: time.Now().Add(-71 * time.Hour), - Local: true, - AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: false, - Language: "en", + ID: "502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + URI: "http://localhost:8080/users/admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + URL: "http://localhost:8080/@admin/statuses/502ccd6f-0edf-48d7-9016-2dfa4d3714cd", + Content: "hello world! #welcome ! first post on the instance :rainbow: !", + Attachments: []string{"b052241b-f30f-4dc6-92fc-2bad0be1f8d8"}, + Tags: []string{"a7e8f5ca-88a1-4652-8079-a187eab8d56e"}, + Mentions: []string{}, + Emojis: []string{"a96ec4f3-1cae-47e4-a508-f9d66a6b221b"}, + CreatedAt: time.Now().Add(-71 * time.Hour), + UpdatedAt: time.Now().Add(-71 * time.Hour), + Local: true, + 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, @@ -738,20 +770,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "admin_account_status_2": { - ID: "0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", - URI: "http://localhost:8080/users/admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", - URL: "http://localhost:8080/@admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", - Content: "🐕🐕🐕🐕🐕", - CreatedAt: time.Now().Add(-70 * time.Hour), - UpdatedAt: time.Now().Add(-70 * time.Hour), - Local: true, - AccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "open to see some puppies", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: true, - Language: "en", + ID: "0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + URI: "http://localhost:8080/users/admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + URL: "http://localhost:8080/@admin/statuses/0fb3f1ac-5cd8-48ac-9050-3d95dc7e44e9", + Content: "🐕🐕🐕🐕🐕", + CreatedAt: time.Now().Add(-70 * time.Hour), + UpdatedAt: time.Now().Add(-70 * time.Hour), + Local: true, + 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, @@ -762,20 +794,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_1_status_1": { - ID: "91b1e795-74ff-4672-a4c4-476616710e2d", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", - URL: "http://localhost:8080/@the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", - Content: "hello everyone!", - CreatedAt: time.Now().Add(-47 * time.Hour), - UpdatedAt: time.Now().Add(-47 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "introduction post", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: true, - Language: "en", + ID: "91b1e795-74ff-4672-a4c4-476616710e2d", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", + URL: "http://localhost:8080/@the_mighty_zork/statuses/91b1e795-74ff-4672-a4c4-476616710e2d", + Content: "hello everyone!", + CreatedAt: time.Now().Add(-47 * time.Hour), + UpdatedAt: time.Now().Add(-47 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "introduction post", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, @@ -786,20 +818,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_1_status_2": { - ID: "3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - URL: "http://localhost:8080/@the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", - Content: "this is an unlocked local-only post that shouldn't federate, but it's still boostable, replyable, and likeable", - CreatedAt: time.Now().Add(-46 * time.Hour), - UpdatedAt: time.Now().Add(-46 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityUnlocked, - Sensitive: false, - Language: "en", + ID: "3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + URL: "http://localhost:8080/@the_mighty_zork/statuses/3dd328d9-8bb1-48f5-bc96-5ccc1c696b4c", + Content: "this is an unlocked local-only post that shouldn't federate, but it's still boostable, replyable, and likeable", + CreatedAt: time.Now().Add(-46 * time.Hour), + UpdatedAt: time.Now().Add(-46 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "", + Visibility: gtsmodel.VisibilityUnlocked, + Sensitive: false, + Language: "en", CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: false, @@ -810,20 +842,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_1_status_3": { - ID: "5e41963f-8ab9-4147-9f00-52d56e19da65", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", - URL: "http://localhost:8080/@the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", - Content: "this is a very personal post that I don't want anyone to interact with at all, and i only want mutuals to see it", - CreatedAt: time.Now().Add(-45 * time.Hour), - UpdatedAt: time.Now().Add(-45 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "test: you shouldn't be able to interact with this post in any way", - Visibility: gtsmodel.VisibilityMutualsOnly, - Sensitive: false, - Language: "en", + ID: "5e41963f-8ab9-4147-9f00-52d56e19da65", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", + URL: "http://localhost:8080/@the_mighty_zork/statuses/5e41963f-8ab9-4147-9f00-52d56e19da65", + Content: "this is a very personal post that I don't want anyone to interact with at all, and i only want mutuals to see it", + CreatedAt: time.Now().Add(-45 * time.Hour), + UpdatedAt: time.Now().Add(-45 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "test: you shouldn't be able to interact with this post in any way", + Visibility: gtsmodel.VisibilityMutualsOnly, + Sensitive: false, + Language: "en", CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, @@ -834,21 +866,21 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_1_status_4": { - ID: "18524c05-97dc-46d7-b474-c811bd9e1e32", - URI: "http://localhost:8080/users/the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32", - URL: "http://localhost:8080/@the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32", - Content: "here's a little gif of trent", - Attachments: []string{"510f6033-798b-4390-81b1-c38ca2205ad3"}, - CreatedAt: time.Now().Add(-1 * time.Hour), - UpdatedAt: time.Now().Add(-1 * time.Hour), - Local: true, - AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "eye contact, trent reznor gif", - Visibility: gtsmodel.VisibilityMutualsOnly, - Sensitive: false, - Language: "en", + ID: "18524c05-97dc-46d7-b474-c811bd9e1e32", + URI: "http://localhost:8080/users/the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32", + URL: "http://localhost:8080/@the_mighty_zork/statuses/18524c05-97dc-46d7-b474-c811bd9e1e32", + Content: "here's a little gif of trent", + Attachments: []string{"510f6033-798b-4390-81b1-c38ca2205ad3"}, + CreatedAt: time.Now().Add(-1 * time.Hour), + UpdatedAt: time.Now().Add(-1 * time.Hour), + Local: true, + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "eye contact, trent reznor gif", + Visibility: gtsmodel.VisibilityMutualsOnly, + Sensitive: false, + Language: "en", CreatedWithApplicationID: "f88697b8-ee3d-46c2-ac3f-dbb85566c3cc", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, @@ -859,20 +891,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_2_status_1": { - ID: "8945ccf2-3873-45e9-aa13-fd7163f19775", - URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", - URL: "http://localhost:8080/@1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", - Content: "🐢 hi everyone i post about turtles 🐢", - CreatedAt: time.Now().Add(-189 * time.Hour), - UpdatedAt: time.Now().Add(-189 * time.Hour), - Local: true, - AccountID: "eecaad73-5703-426d-9312-276641daa31e", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "introduction post", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: true, - Language: "en", + ID: "8945ccf2-3873-45e9-aa13-fd7163f19775", + URI: "http://localhost:8080/users/1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", + URL: "http://localhost:8080/@1happyturtle/statuses/8945ccf2-3873-45e9-aa13-fd7163f19775", + Content: "🐢 hi everyone i post about turtles 🐢", + CreatedAt: time.Now().Add(-189 * time.Hour), + UpdatedAt: time.Now().Add(-189 * time.Hour), + Local: true, + AccountID: "eecaad73-5703-426d-9312-276641daa31e", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "introduction post", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, @@ -883,20 +915,20 @@ func NewTestStatuses() map[string]*gtsmodel.Status { ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, "local_account_2_status_2": { - ID: "c7e25a86-f0d3-4705-a73c-c597f687d3dd", - URI: "http://localhost:8080/users/1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", - URL: "http://localhost:8080/@1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", - Content: "🐢 this one is federated, likeable, and boostable but not replyable 🐢", - CreatedAt: time.Now().Add(-1 * time.Minute), - UpdatedAt: time.Now().Add(-1 * time.Minute), - Local: true, - AccountID: "eecaad73-5703-426d-9312-276641daa31e", - InReplyToID: "", - BoostOfID: "", - ContentWarning: "", - Visibility: gtsmodel.VisibilityPublic, - Sensitive: true, - Language: "en", + ID: "c7e25a86-f0d3-4705-a73c-c597f687d3dd", + URI: "http://localhost:8080/users/1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", + URL: "http://localhost:8080/@1happyturtle/statuses/c7e25a86-f0d3-4705-a73c-c597f687d3dd", + Content: "🐢 this one is federated, likeable, and boostable but not replyable 🐢", + CreatedAt: time.Now().Add(-1 * time.Minute), + UpdatedAt: time.Now().Add(-1 * time.Minute), + Local: true, + AccountID: "eecaad73-5703-426d-9312-276641daa31e", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "", + Visibility: gtsmodel.VisibilityPublic, + Sensitive: true, + Language: "en", CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df", VisibilityAdvanced: >smodel.VisibilityAdvanced{ Federated: true, @@ -906,9 +938,34 @@ func NewTestStatuses() map[string]*gtsmodel.Status { }, ActivityStreamsType: gtsmodel.ActivityStreamsNote, }, + "local_account_2_status_3": { + ID: "75960e30-7a8e-4f45-87fa-440a4d1c9572", + URI: "http://localhost:8080/users/1happyturtle/statuses/75960e30-7a8e-4f45-87fa-440a4d1c9572", + URL: "http://localhost:8080/@1happyturtle/statuses/75960e30-7a8e-4f45-87fa-440a4d1c9572", + Content: "🐢 i don't mind people sharing this one but I don't want likes or replies to it because cba🐢", + CreatedAt: time.Now().Add(-2 * time.Minute), + UpdatedAt: time.Now().Add(-2 * time.Minute), + Local: true, + AccountID: "eecaad73-5703-426d-9312-276641daa31e", + InReplyToID: "", + BoostOfID: "", + ContentWarning: "you won't be able to like or reply to this", + Visibility: gtsmodel.VisibilityUnlocked, + Sensitive: true, + Language: "en", + CreatedWithApplicationID: "6b0cd164-8497-4cd5-bec9-957886fac5df", + VisibilityAdvanced: >smodel.VisibilityAdvanced{ + Federated: true, + Boostable: true, + Replyable: false, + Likeable: false, + }, + ActivityStreamsType: gtsmodel.ActivityStreamsNote, + }, } } +// NewTestTags returns a map of gts model tags keyed by their name func NewTestTags() map[string]*gtsmodel.Tag { return map[string]*gtsmodel.Tag{ "welcome": { @@ -923,3 +980,16 @@ func NewTestTags() map[string]*gtsmodel.Tag { }, } } + +// NewTestFaves returns a map of gts model faves, keyed in the format [faving_account]_[target_status] +func NewTestFaves() map[string]*gtsmodel.StatusFave { + return map[string]*gtsmodel.StatusFave{ + "local_account_1_admin_account_status_1": { + ID: "fc4d42ef-631c-4125-bd9d-88695131284c", + CreatedAt: time.Now().Add(-47 * time.Hour), + AccountID: "580072df-4d03-4684-a412-89fd6f7d77e6", // local account 1 + TargetAccountID: "8020dbb4-1e7b-4d99-a872-4cf94e64210f", // admin account + StatusID: "502ccd6f-0edf-48d7-9016-2dfa4d3714cd", // admin account status 1 + }, + } +}