phew
This commit is contained in:
parent
82437a768b
commit
ba84852d3e
|
@ -37,7 +37,7 @@ const (
|
|||
mediaSizeKey = "media_size"
|
||||
fileNameKey = "file_name"
|
||||
|
||||
filesPath = "files"
|
||||
filesPath = "files"
|
||||
)
|
||||
|
||||
// fileServer implements the RESTAPIModule interface.
|
||||
|
|
|
@ -29,7 +29,7 @@ import (
|
|||
// module implements the apiclient interface
|
||||
type module struct {
|
||||
config *config.Config
|
||||
log *logrus.Logger
|
||||
log *logrus.Logger
|
||||
}
|
||||
|
||||
// New returns a new security module
|
||||
|
|
|
@ -21,7 +21,9 @@ package status
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
|
@ -77,7 +79,7 @@ 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)
|
||||
r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -89,6 +91,10 @@ func (m *statusModule) CreateTables(db db.DB) error {
|
|||
>smodel.Follow{},
|
||||
>smodel.FollowRequest{},
|
||||
>smodel.Status{},
|
||||
>smodel.StatusFave{},
|
||||
>smodel.StatusBookmark{},
|
||||
>smodel.StatusMute{},
|
||||
>smodel.StatusPin{},
|
||||
>smodel.Application{},
|
||||
>smodel.EmailDomainBlock{},
|
||||
>smodel.MediaAttachment{},
|
||||
|
@ -105,13 +111,14 @@ func (m *statusModule) CreateTables(db db.DB) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// func (m *statusModule) 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)
|
||||
// }
|
||||
// }
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -97,18 +97,20 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
|||
thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
|
||||
thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
|
||||
newStatus := >smodel.Status{
|
||||
ID: thisStatusID,
|
||||
URI: thisStatusURI,
|
||||
URL: thisStatusURL,
|
||||
Content: util.HTMLFormat(form.Status),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Local: true,
|
||||
AccountID: authed.Account.ID,
|
||||
ContentWarning: form.SpoilerText,
|
||||
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||
Sensitive: form.Sensitive,
|
||||
Language: form.Language,
|
||||
ID: thisStatusID,
|
||||
URI: thisStatusURI,
|
||||
URL: thisStatusURL,
|
||||
Content: util.HTMLFormat(form.Status),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
Local: true,
|
||||
AccountID: authed.Account.ID,
|
||||
ContentWarning: form.SpoilerText,
|
||||
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||
Sensitive: form.Sensitive,
|
||||
Language: form.Language,
|
||||
CreatedWithApplicationID: authed.Application.ID,
|
||||
Text: form.Status,
|
||||
}
|
||||
|
||||
// check if replyToID is ok
|
||||
|
@ -181,80 +183,11 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
|||
/*
|
||||
FROM THIS POINT ONWARDS WE ARE JUST CREATING THE FRONTEND REPRESENTATION OF THE STATUS TO RETURN TO THE SUBMITTER
|
||||
*/
|
||||
mastoVis := util.ParseMastoVisFromGTSVis(newStatus.Visibility)
|
||||
|
||||
mastoAccount, err := m.mastoConverter.AccountToMastoPublic(authed.Account)
|
||||
mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, newStatus.GTSReplyToStatus)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mastoAttachments := []mastotypes.Attachment{}
|
||||
for _, a := range newStatus.GTSMediaAttachments {
|
||||
ma, err := m.mastoConverter.AttachmentToMasto(a)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mastoAttachments = append(mastoAttachments, ma)
|
||||
}
|
||||
|
||||
mastoMentions := []mastotypes.Mention{}
|
||||
for _, gtsm := range newStatus.GTSMentions {
|
||||
mm, err := m.mastoConverter.MentionToMasto(gtsm)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mastoMentions = append(mastoMentions, mm)
|
||||
}
|
||||
|
||||
mastoApplication, err := m.mastoConverter.AppToMastoPublic(authed.Application)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mastoTags := []mastotypes.Tag{}
|
||||
for _, gtst := range newStatus.GTSTags {
|
||||
mt, err := m.mastoConverter.TagToMasto(gtst)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mastoTags = append(mastoTags, mt)
|
||||
}
|
||||
|
||||
mastoEmojis := []mastotypes.Emoji{}
|
||||
for _, gtse := range newStatus.GTSEmojis {
|
||||
me, err := m.mastoConverter.EmojiToMasto(gtse)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
mastoEmojis = append(mastoEmojis, me)
|
||||
}
|
||||
|
||||
mastoStatus := &mastotypes.Status{
|
||||
ID: newStatus.ID,
|
||||
CreatedAt: newStatus.CreatedAt.Format(time.RFC3339),
|
||||
InReplyToID: newStatus.InReplyToID,
|
||||
InReplyToAccountID: newStatus.InReplyToAccountID,
|
||||
Sensitive: newStatus.Sensitive,
|
||||
SpoilerText: newStatus.ContentWarning,
|
||||
Visibility: mastoVis,
|
||||
Language: newStatus.Language,
|
||||
URI: newStatus.URI,
|
||||
URL: newStatus.URL,
|
||||
Content: newStatus.Content,
|
||||
Application: mastoApplication,
|
||||
Account: mastoAccount,
|
||||
MediaAttachments: mastoAttachments,
|
||||
Mentions: mastoMentions,
|
||||
Tags: mastoTags,
|
||||
Emojis: mastoEmojis,
|
||||
Text: form.Status,
|
||||
}
|
||||
c.JSON(http.StatusOK, mastoStatus)
|
||||
}
|
||||
|
||||
|
@ -487,7 +420,7 @@ func (m *statusModule) parseMentions(form *advancedStatusCreateForm, accountID s
|
|||
if err := m.db.Put(menchie); err != nil {
|
||||
return fmt.Errorf("error putting mentions in db: %s", err)
|
||||
}
|
||||
menchies = append(menchies, menchie.ID)
|
||||
menchies = append(menchies, menchie.TargetAccountID)
|
||||
}
|
||||
// add full populated gts menchies to the status for passing them around conveniently
|
||||
status.GTSMentions = gtsMenchies
|
||||
|
|
|
@ -162,7 +162,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
|||
assert.Len(suite.T(), statusReply.Tags, 1)
|
||||
assert.Equal(suite.T(), mastomodel.Tag{
|
||||
Name: "helloworld",
|
||||
URL: "http://localhost:8080/tags/helloworld",
|
||||
URL: "http://localhost:8080/tags/helloworld",
|
||||
}, statusReply.Tags[0])
|
||||
|
||||
gtsTag := >smodel.Tag{}
|
||||
|
@ -185,7 +185,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
|
|||
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.Form = url.Values{
|
||||
"status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
|
||||
"status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
|
||||
}
|
||||
suite.statusModule.statusCreatePOSTHandler(ctx)
|
||||
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
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/oauth"
|
||||
)
|
||||
|
||||
func (m *statusModule) statusGETHandler(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
|
||||
}
|
||||
|
||||
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, requestingAccount, 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
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, mastoStatus)
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
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 (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"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"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type StatusGetTestSuite 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
|
||||
|
||||
// module being tested
|
||||
statusModule *statusModule
|
||||
}
|
||||
|
||||
/*
|
||||
TEST INFRASTRUCTURE
|
||||
*/
|
||||
|
||||
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
|
||||
func (suite *StatusGetTestSuite) 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 = New(suite.config, suite.db, suite.oauthServer, suite.mediaHandler, suite.mastoConverter, suite.distributor, suite.log).(*statusModule)
|
||||
}
|
||||
|
||||
func (suite *StatusGetTestSuite) TearDownSuite() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
}
|
||||
|
||||
func (suite *StatusGetTestSuite) 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()
|
||||
}
|
||||
|
||||
// TearDownTest drops tables to make sure there's no data in the db
|
||||
func (suite *StatusGetTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
}
|
||||
|
||||
/*
|
||||
ACTUAL TESTS
|
||||
*/
|
||||
|
||||
/*
|
||||
TESTING: StatusGetPOSTHandler
|
||||
*/
|
||||
|
||||
// Post a new status with some custom visibility settings
|
||||
func (suite *StatusGetTestSuite) TestPostNewStatus() {
|
||||
|
||||
// t := suite.testTokens["local_account_1"]
|
||||
// oauthToken := oauth.PGTokenToOauthToken(t)
|
||||
|
||||
// // 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", basePath), nil) // the endpoint we're hitting
|
||||
// ctx.Request.Form = url.Values{
|
||||
// "status": {"this is a brand new status! #helloworld"},
|
||||
// "spoiler_text": {"hello hello"},
|
||||
// "sensitive": {"true"},
|
||||
// "visibility_advanced": {"mutuals_only"},
|
||||
// "likeable": {"false"},
|
||||
// "replyable": {"false"},
|
||||
// "federated": {"false"},
|
||||
// }
|
||||
// suite.statusModule.statusGETHandler(ctx)
|
||||
|
||||
// // check response
|
||||
|
||||
// // 1. we should have OK from our call to the function
|
||||
// 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(), "hello hello", statusReply.SpoilerText)
|
||||
// assert.Equal(suite.T(), "this is a brand new status! #helloworld", statusReply.Content)
|
||||
// assert.True(suite.T(), statusReply.Sensitive)
|
||||
// assert.Equal(suite.T(), mastomodel.VisibilityPrivate, statusReply.Visibility)
|
||||
// assert.Len(suite.T(), statusReply.Tags, 1)
|
||||
// assert.Equal(suite.T(), mastomodel.Tag{
|
||||
// Name: "helloworld",
|
||||
// URL: "http://localhost:8080/tags/helloworld",
|
||||
// }, statusReply.Tags[0])
|
||||
|
||||
// gtsTag := >smodel.Tag{}
|
||||
// err = suite.db.GetWhere("name", "helloworld", gtsTag)
|
||||
// assert.NoError(suite.T(), err)
|
||||
// assert.Equal(suite.T(), statusReply.Account.ID, gtsTag.FirstSeenFromAccountID)
|
||||
}
|
||||
|
||||
func TestStatusGetTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(StatusGetTestSuite))
|
||||
}
|
|
@ -187,6 +187,54 @@ type DB interface {
|
|||
// That is, it returns true if account1 blocks account2, OR if account2 blocks account1.
|
||||
Blocked(account1 string, account2 string) (bool, error)
|
||||
|
||||
// StatusVisible returns true if targetStatus is visible to requestingAccount, based on the
|
||||
// privacy settings of the status, and any blocks/mutes that might exist between the two accounts
|
||||
// or account domains.
|
||||
//
|
||||
// StatusVisible will also check through the given slice of 'otherRelevantAccounts', which should include:
|
||||
//
|
||||
// 1. Accounts mentioned in the targetStatus
|
||||
//
|
||||
// 2. Accounts replied to by the target status
|
||||
//
|
||||
// 3. Accounts boosted by the target status
|
||||
//
|
||||
// Will return an error if something goes wrong while pulling stuff out of the database.
|
||||
StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error)
|
||||
|
||||
// Follows returns true if sourceAccount follows target account, or an error if something goes wrong while finding out.
|
||||
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||
|
||||
// Mutuals returns true if account1 and account2 both follow each other, or an error if something goes wrong while finding out.
|
||||
Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error)
|
||||
|
||||
// PullRelevantAccountsFromStatus returns all accounts mentioned in a status, replied to by a status, or boosted by a status
|
||||
PullRelevantAccountsFromStatus(status *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error)
|
||||
|
||||
// GetReplyCountForStatus returns the amount of replies recorded for a status, or an error if something goes wrong
|
||||
GetReplyCountForStatus(status *gtsmodel.Status) (int, error)
|
||||
|
||||
// GetReblogCountForStatus returns the amount of reblogs/boosts recorded for a status, or an error if something goes wrong
|
||||
GetReblogCountForStatus(status *gtsmodel.Status) (int, error)
|
||||
|
||||
// GetFaveCountForStatus returns the amount of faves/likes recorded for a status, or an error if something goes wrong
|
||||
GetFaveCountForStatus(status *gtsmodel.Status) (int, error)
|
||||
|
||||
// StatusFavedBy checks if a given status has been faved by a given account ID
|
||||
StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// StatusRebloggedBy checks if a given status has been reblogged/boosted by a given account ID
|
||||
StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// StatusMutedBy checks if a given status has been muted by a given account ID
|
||||
StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
||||
StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
// StatusPinnedBy checks if a given status has been pinned by a given account ID
|
||||
StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||
|
||||
/*
|
||||
USEFUL CONVERSION FUNCTIONS
|
||||
*/
|
||||
|
|
|
@ -34,7 +34,7 @@ type Status struct {
|
|||
Attachments []string `pg:",array"`
|
||||
// Database IDs of any tags used in this status
|
||||
Tags []string `pg:",array"`
|
||||
// Database IDs of any mentions in this status
|
||||
// Database IDs of any accounts mentioned in this status
|
||||
Mentions []string `pg:",array"`
|
||||
// Database IDs of any emojis used in this status
|
||||
Emojis []string `pg:",array"`
|
||||
|
@ -60,11 +60,15 @@ type Status struct {
|
|||
Sensitive bool
|
||||
// what language is this status written in?
|
||||
Language string
|
||||
// Which application was used to create this status?
|
||||
CreatedWithApplicationID string
|
||||
// advanced visibility for this status
|
||||
VisibilityAdvanced *VisibilityAdvanced
|
||||
// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
// Will probably almost always be Note but who knows!.
|
||||
ActivityStreamsType ActivityStreamsObject
|
||||
// Original text of the status without formatting
|
||||
Text string
|
||||
|
||||
/*
|
||||
NON-DATABASE FIELDS
|
||||
|
@ -105,6 +109,7 @@ const (
|
|||
VisibilityDefault Visibility = "public"
|
||||
)
|
||||
|
||||
// VisibilityAdvanced denotes a set of flags that can be set on a status for fine-tuning visibility and interactivity of the status.
|
||||
type VisibilityAdvanced struct {
|
||||
/*
|
||||
ADVANCED SETTINGS -- These should all default to TRUE.
|
||||
|
@ -123,3 +128,11 @@ type VisibilityAdvanced struct {
|
|||
// This status can be liked/faved
|
||||
Likeable bool `pg:"default:true"`
|
||||
}
|
||||
|
||||
// RelevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status.
|
||||
type RelevantAccounts struct {
|
||||
ReplyToAccount *Account
|
||||
BoostedAccount *Account
|
||||
BoostedReplyToAccount *Account
|
||||
MentionedAccounts []*Account
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// StatusBookmark refers to one account having a 'bookmark' of the status of another account
|
||||
type StatusBookmark struct {
|
||||
// id of this bookmark in the database
|
||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
||||
// when was this bookmark created
|
||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// id of the account that created ('did') the bookmarking
|
||||
AccountID string `pg:",notnull"`
|
||||
// id the account owning the bookmarked status
|
||||
TargetAccountID string `pg:",notnull"`
|
||||
// database id of the status that has been bookmarked
|
||||
StatusID string `pg:",notnull"`
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 gtsmodel
|
||||
|
||||
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"`
|
||||
// when was this fave created
|
||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// id of the account that created ('did') the fave
|
||||
AccountID string `pg:",notnull"`
|
||||
// id the account owning the faved status
|
||||
TargetAccountID string `pg:",notnull"`
|
||||
// database id of the status that has been 'faved'
|
||||
StatusID string `pg:",notnull"`
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// StatusMute refers to one account having muted the status of another account or its own
|
||||
type StatusMute struct {
|
||||
// id of this mute in the database
|
||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
||||
// when was this mute created
|
||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// id of the account that created ('did') the mute
|
||||
AccountID string `pg:",notnull"`
|
||||
// id the account owning the muted status (can be the same as accountID)
|
||||
TargetAccountID string `pg:",notnull"`
|
||||
// database id of the status that has been muted
|
||||
StatusID string `pg:",notnull"`
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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 gtsmodel
|
||||
|
||||
import "time"
|
||||
|
||||
// StatusPin refers to a status 'pinned' to the top of an account
|
||||
type StatusPin struct {
|
||||
// id of this pin in the database
|
||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
||||
// when was this pin created
|
||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||
// id of the account that created ('did') the pinning (this should always be the same as the author of the status)
|
||||
AccountID string `pg:",notnull"`
|
||||
// database id of the status that has been pinned
|
||||
StatusID string `pg:",notnull"`
|
||||
}
|
|
@ -38,7 +38,6 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
|
||||
mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
@ -556,160 +555,266 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro
|
|||
return blocked, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
||||
l := ps.log.WithField("func", "StatusVisible")
|
||||
|
||||
// if target account is suspended then don't show the status
|
||||
if !targetAccount.SuspendedAt.IsZero() {
|
||||
l.Debug("target account suspended at is not zero")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// if the target user doesn't exist (anymore) then the status also shouldn't be visible
|
||||
targetUser := >smodel.User{}
|
||||
if err := ps.conn.Model(targetUser).Where("account_id = ?", targetAccount.ID).Select(); err != nil {
|
||||
l.Debug("target user could not be selected")
|
||||
if err == pg.ErrNoRows {
|
||||
return false, ErrNoEntries{}
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// if target user is disabled, not yet approved, or not confirmed then don't show the status
|
||||
// (although in the latter two cases it's unlikely they posted a status yet anyway, but you never know!)
|
||||
if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() {
|
||||
l.Debug("target user is disabled, not approved, or not confirmed")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check if we have a local account -- if so we can check the user for that account in the DB
|
||||
if requestingAccount.Domain == "" {
|
||||
requestingUser := >smodel.User{}
|
||||
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 {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
// okay, user exists, so make sure it has full privileges/is confirmed/approved
|
||||
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
|
||||
return false, 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
|
||||
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
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check other accounts mentioned/boosted by/replied to by the status, if they exist
|
||||
if relevantAccounts != nil {
|
||||
// status replies to account id
|
||||
if relevantAccounts.ReplyToAccount != nil {
|
||||
if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// status boosts accounts id
|
||||
if relevantAccounts.BoostedAccount != nil {
|
||||
if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// status boosts a reply to account id
|
||||
if relevantAccounts.BoostedReplyToAccount != nil {
|
||||
if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// status mentions accounts
|
||||
for _, a := range relevantAccounts.MentionedAccounts {
|
||||
if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil {
|
||||
return false, err
|
||||
} else if blocked {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we know neither account blocks the other, or another account mentioned or otherwise referred to in the status
|
||||
// that means it's now just a matter of checking the visibility settings of the status itself
|
||||
switch targetStatus.Visibility {
|
||||
case gtsmodel.VisibilityPublic, gtsmodel.VisibilityUnlocked:
|
||||
// no problem here, just return OK
|
||||
return true, nil
|
||||
case gtsmodel.VisibilityFollowersOnly:
|
||||
// check one-way follow
|
||||
follows, err := ps.Follows(requestingAccount, targetAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !follows {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
case gtsmodel.VisibilityMutualsOnly:
|
||||
// check mutual follow
|
||||
mutuals, err := ps.Mutuals(requestingAccount, targetAccount)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !mutuals {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
case gtsmodel.VisibilityDirect:
|
||||
// make sure the requesting account is mentioned in the status
|
||||
for _, menchie := range targetStatus.Mentions {
|
||||
if menchie == requestingAccount.ID {
|
||||
return true, nil // yep it's mentioned!
|
||||
}
|
||||
}
|
||||
return false, nil // it's not mentioned -_-
|
||||
}
|
||||
|
||||
return false, errors.New("reached the end of StatusVisible with no result")
|
||||
}
|
||||
|
||||
func (ps *postgresService) Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error) {
|
||||
return ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", sourceAccount.ID).Where("target_account_id = ?", targetAccount.ID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) Mutuals(account1 *gtsmodel.Account, account2 *gtsmodel.Account) (bool, error) {
|
||||
// make sure account 1 follows account 2
|
||||
f1, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account1.ID).Where("target_account_id = ?", account2.ID).Exists()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
// make sure account 2 follows account 1
|
||||
f2, err := ps.conn.Model(>smodel.Follow{}).Where("account_id = ?", account2.ID).Where("target_account_id = ?", account1.ID).Exists()
|
||||
if err != nil {
|
||||
if err == pg.ErrNoRows {
|
||||
return false, nil
|
||||
} else {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return f1 && f2, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel.Status) (*gtsmodel.RelevantAccounts, error) {
|
||||
accounts := >smodel.RelevantAccounts{
|
||||
MentionedAccounts: []*gtsmodel.Account{},
|
||||
}
|
||||
|
||||
// get the replied to account from the status and add it to the pile
|
||||
if targetStatus.InReplyToAccountID != "" {
|
||||
repliedToAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(repliedToAccount).Where("id = ?", targetStatus.InReplyToAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.ReplyToAccount = repliedToAccount
|
||||
}
|
||||
|
||||
// get the boosted account from the status and add it to the pile
|
||||
if targetStatus.BoostOfID != "" {
|
||||
// retrieve the boosted status first
|
||||
boostedStatus := >smodel.Status{}
|
||||
if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
boostedAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(boostedAccount).Where("id = ?", boostedStatus.AccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.BoostedAccount = boostedAccount
|
||||
|
||||
// the boosted status might be a reply to another account so we should get that too
|
||||
if boostedStatus.InReplyToAccountID != "" {
|
||||
boostedStatusRepliedToAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(boostedStatusRepliedToAccount).Where("id = ?", boostedStatus.InReplyToAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.BoostedReplyToAccount = boostedStatusRepliedToAccount
|
||||
}
|
||||
}
|
||||
|
||||
// now get all accounts with IDs that are mentioned in the status
|
||||
for _, mentionedAccountID := range targetStatus.Mentions {
|
||||
mentionedAccount := >smodel.Account{}
|
||||
if err := ps.conn.Model(mentionedAccount).Where("id = ?", mentionedAccountID).Select(); err != nil {
|
||||
return accounts, err
|
||||
}
|
||||
accounts.MentionedAccounts = append(accounts.MentionedAccounts, mentionedAccount)
|
||||
}
|
||||
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetReplyCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) {
|
||||
return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Count()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusRebloggedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.Status{}).Where("boost_of_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusMutedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusMute{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
func (ps *postgresService) StatusPinnedBy(status *gtsmodel.Status, accountID string) (bool, error) {
|
||||
return ps.conn.Model(>smodel.StatusPin{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||
}
|
||||
|
||||
/*
|
||||
CONVERSION FUNCTIONS
|
||||
*/
|
||||
|
||||
// AccountToMastoSensitive takes an internal account model and transforms it into an account ready to be served through the API.
|
||||
// The resulting account fits the specifications for the path /api/v1/accounts/verify_credentials, as described here:
|
||||
// https://docs.joinmastodon.org/methods/accounts/. Note that it's *sensitive* because it's only meant to be exposed to the user
|
||||
// that the account actually belongs to.
|
||||
func (ps *postgresService) AccountToMastoSensitive(a *gtsmodel.Account) (*mastotypes.Account, error) {
|
||||
// we can build this sensitive account easily by first getting the public account....
|
||||
mastoAccount, err := ps.AccountToMastoPublic(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// then adding the Source object to it...
|
||||
|
||||
// check pending follow requests aimed at this account
|
||||
fr := []gtsmodel.FollowRequest{}
|
||||
if err := ps.GetFollowRequestsForAccountID(a.ID, &fr); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting follow requests: %s", err)
|
||||
}
|
||||
}
|
||||
var frc int
|
||||
if fr != nil {
|
||||
frc = len(fr)
|
||||
}
|
||||
|
||||
mastoAccount.Source = &mastotypes.Source{
|
||||
Privacy: util.ParseMastoVisFromGTSVis(a.Privacy),
|
||||
Sensitive: a.Sensitive,
|
||||
Language: a.Language,
|
||||
Note: a.Note,
|
||||
Fields: mastoAccount.Fields,
|
||||
FollowRequestsCount: frc,
|
||||
}
|
||||
|
||||
return mastoAccount, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) AccountToMastoPublic(a *gtsmodel.Account) (*mastotypes.Account, error) {
|
||||
// count followers
|
||||
followers := []gtsmodel.Follow{}
|
||||
if err := ps.GetFollowersByAccountID(a.ID, &followers); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting followers: %s", err)
|
||||
}
|
||||
}
|
||||
var followersCount int
|
||||
if followers != nil {
|
||||
followersCount = len(followers)
|
||||
}
|
||||
|
||||
// count following
|
||||
following := []gtsmodel.Follow{}
|
||||
if err := ps.GetFollowingByAccountID(a.ID, &following); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting following: %s", err)
|
||||
}
|
||||
}
|
||||
var followingCount int
|
||||
if following != nil {
|
||||
followingCount = len(following)
|
||||
}
|
||||
|
||||
// count statuses
|
||||
statuses := []gtsmodel.Status{}
|
||||
if err := ps.GetStatusesByAccountID(a.ID, &statuses); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting last statuses: %s", err)
|
||||
}
|
||||
}
|
||||
var statusesCount int
|
||||
if statuses != nil {
|
||||
statusesCount = len(statuses)
|
||||
}
|
||||
|
||||
// check when the last status was
|
||||
lastStatus := >smodel.Status{}
|
||||
if err := ps.GetLastStatusForAccountID(a.ID, lastStatus); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting last status: %s", err)
|
||||
}
|
||||
}
|
||||
var lastStatusAt string
|
||||
if lastStatus != nil {
|
||||
lastStatusAt = lastStatus.CreatedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// build the avatar and header URLs
|
||||
avi := >smodel.MediaAttachment{}
|
||||
if err := ps.GetAvatarForAccountID(avi, a.ID); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting avatar: %s", err)
|
||||
}
|
||||
}
|
||||
aviURL := avi.File.Path
|
||||
aviURLStatic := avi.Thumbnail.Path
|
||||
|
||||
header := >smodel.MediaAttachment{}
|
||||
if err := ps.GetHeaderForAccountID(avi, a.ID); err != nil {
|
||||
if _, ok := err.(ErrNoEntries); !ok {
|
||||
return nil, fmt.Errorf("error getting header: %s", err)
|
||||
}
|
||||
}
|
||||
headerURL := header.File.Path
|
||||
headerURLStatic := header.Thumbnail.Path
|
||||
|
||||
// get the fields set on this account
|
||||
fields := []mastotypes.Field{}
|
||||
for _, f := range a.Fields {
|
||||
mField := mastotypes.Field{
|
||||
Name: f.Name,
|
||||
Value: f.Value,
|
||||
}
|
||||
if !f.VerifiedAt.IsZero() {
|
||||
mField.VerifiedAt = f.VerifiedAt.Format(time.RFC3339)
|
||||
}
|
||||
fields = append(fields, mField)
|
||||
}
|
||||
|
||||
var acct string
|
||||
if a.Domain != "" {
|
||||
// this is a remote user
|
||||
acct = fmt.Sprintf("%s@%s", a.Username, a.Domain)
|
||||
} else {
|
||||
// this is a local user
|
||||
acct = a.Username
|
||||
}
|
||||
|
||||
return &mastotypes.Account{
|
||||
ID: a.ID,
|
||||
Username: a.Username,
|
||||
Acct: acct,
|
||||
DisplayName: a.DisplayName,
|
||||
Locked: a.Locked,
|
||||
Bot: a.Bot,
|
||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||
Note: a.Note,
|
||||
URL: a.URL,
|
||||
Avatar: aviURL,
|
||||
AvatarStatic: aviURLStatic,
|
||||
Header: headerURL,
|
||||
HeaderStatic: headerURLStatic,
|
||||
FollowersCount: followersCount,
|
||||
FollowingCount: followingCount,
|
||||
StatusesCount: statusesCount,
|
||||
LastStatusAt: lastStatusAt,
|
||||
Emojis: nil, // TODO: implement this
|
||||
Fields: fields,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
|
||||
menchies := []*gtsmodel.Mention{}
|
||||
for _, a := range targetAccounts {
|
||||
|
|
|
@ -63,6 +63,9 @@ type Converter interface {
|
|||
|
||||
// TagToMasto converts a gts model tag into its mastodon (frontend) representation for serialization on the API.
|
||||
TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error)
|
||||
|
||||
// StatusToMasto converts a gts model status into its mastodon (frontend) representation for serialization on the API.
|
||||
StatusToMasto(s *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, boostOfAccount *gtsmodel.Account, replyToAccount *gtsmodel.Account, reblogOfStatus *gtsmodel.Status) (*mastotypes.Status, error)
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
|
@ -318,3 +321,211 @@ func (c *converter) TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error) {
|
|||
URL: tagURL, // we don't serve URLs with collections of tagged statuses (FOR NOW) so this is purely for mastodon compatibility ¯\_(ツ)_/¯
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *converter) StatusToMasto(
|
||||
s *gtsmodel.Status,
|
||||
targetAccount *gtsmodel.Account,
|
||||
requestingAccount *gtsmodel.Account,
|
||||
boostOfAccount *gtsmodel.Account,
|
||||
replyToAccount *gtsmodel.Account,
|
||||
reblogOfStatus *gtsmodel.Status) (*mastotypes.Status, error) {
|
||||
|
||||
repliesCount, err := c.db.GetReplyCountForStatus(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error counting replies: %s", err)
|
||||
}
|
||||
|
||||
reblogsCount, err := c.db.GetReblogCountForStatus(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error counting reblogs: %s", err)
|
||||
}
|
||||
|
||||
favesCount, err := c.db.GetFaveCountForStatus(s)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
bookmarked, err := c.db.StatusBookmarkedBy(s, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has bookmarked status: %s", err)
|
||||
}
|
||||
|
||||
pinned, err := c.db.StatusPinnedBy(s, requestingAccount.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error checking if requesting account has pinned status: %s", err)
|
||||
}
|
||||
|
||||
var mastoRebloggedStatus *mastotypes.Status // TODO
|
||||
|
||||
application := >smodel.Application{}
|
||||
if err := c.db.GetByID(s.CreatedWithApplicationID, application); err != nil {
|
||||
return nil, fmt.Errorf("error fetching application used to create status: %s", err)
|
||||
}
|
||||
mastoApplication, err := c.AppToMastoPublic(application)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing application used to create status: %s", err)
|
||||
}
|
||||
|
||||
mastoTargetAccount, err := c.AccountToMastoPublic(targetAccount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing account of status author: %s", err)
|
||||
}
|
||||
|
||||
mastoAttachments := []mastotypes.Attachment{}
|
||||
// the status might already have some gts attachments on it if it's not been pulled directly from the database
|
||||
// if so, we can directly convert the gts attachments into masto ones
|
||||
if s.GTSMediaAttachments != nil {
|
||||
for _, gtsAttachment := range s.GTSMediaAttachments {
|
||||
mastoAttachment, err := c.AttachmentToMasto(gtsAttachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting attachment with id %s: %s", gtsAttachment.ID, err)
|
||||
}
|
||||
mastoAttachments = append(mastoAttachments, mastoAttachment)
|
||||
}
|
||||
// the status doesn't have gts attachments on it, but it does have attachment IDs
|
||||
// in this case, we need to pull the gts attachments from the db to convert them into masto ones
|
||||
} else {
|
||||
for _, a := range s.Attachments {
|
||||
gtsAttachment := >smodel.MediaAttachment{}
|
||||
if err := c.db.GetByID(a, gtsAttachment); err != nil {
|
||||
return nil, fmt.Errorf("error getting attachment with id %s: %s", a, err)
|
||||
}
|
||||
mastoAttachment, err := c.AttachmentToMasto(gtsAttachment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting attachment with id %s: %s", a, err)
|
||||
}
|
||||
mastoAttachments = append(mastoAttachments, mastoAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
mastoMentions := []mastotypes.Mention{}
|
||||
// the status might already have some gts mentions on it if it's not been pulled directly from the database
|
||||
// if so, we can directly convert the gts mentions into masto ones
|
||||
if s.GTSMentions != nil {
|
||||
for _, gtsMention := range s.GTSMentions {
|
||||
mastoMention, err := c.MentionToMasto(gtsMention)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting mention with id %s: %s", gtsMention.ID, err)
|
||||
}
|
||||
mastoMentions = append(mastoMentions, mastoMention)
|
||||
}
|
||||
// the status doesn't have gts mentions on it, but it does have mention IDs
|
||||
// in this case, we need to pull the gts mentions from the db to convert them into masto ones
|
||||
} else {
|
||||
for _, m := range s.Mentions {
|
||||
gtsMention := >smodel.Mention{}
|
||||
if err := c.db.GetByID(m, gtsMention); err != nil {
|
||||
return nil, fmt.Errorf("error getting mention with id %s: %s", m, err)
|
||||
}
|
||||
mastoMention, err := c.MentionToMasto(gtsMention)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting mention with id %s: %s", gtsMention.ID, err)
|
||||
}
|
||||
mastoMentions = append(mastoMentions, mastoMention)
|
||||
}
|
||||
}
|
||||
|
||||
mastoTags := []mastotypes.Tag{}
|
||||
// the status might already have some gts tags on it if it's not been pulled directly from the database
|
||||
// if so, we can directly convert the gts tags into masto ones
|
||||
if s.GTSTags != nil {
|
||||
for _, gtsTag := range s.GTSTags {
|
||||
mastoTag, err := c.TagToMasto(gtsTag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting tag with id %s: %s", gtsTag.ID, err)
|
||||
}
|
||||
mastoTags = append(mastoTags, mastoTag)
|
||||
}
|
||||
// the status doesn't have gts tags on it, but it does have tag IDs
|
||||
// in this case, we need to pull the gts tags from the db to convert them into masto ones
|
||||
} else {
|
||||
for _, t := range s.Tags {
|
||||
gtsTag := >smodel.Tag{}
|
||||
if err := c.db.GetByID(t, gtsTag); err != nil {
|
||||
return nil, fmt.Errorf("error getting tag with id %s: %s", t, err)
|
||||
}
|
||||
mastoTag, err := c.TagToMasto(gtsTag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting tag with id %s: %s", gtsTag.ID, err)
|
||||
}
|
||||
mastoTags = append(mastoTags, mastoTag)
|
||||
}
|
||||
}
|
||||
|
||||
mastoEmojis := []mastotypes.Emoji{}
|
||||
// the status might already have some gts emojis on it if it's not been pulled directly from the database
|
||||
// if so, we can directly convert the gts emojis into masto ones
|
||||
if s.GTSEmojis != nil {
|
||||
for _, gtsEmoji := range s.GTSEmojis {
|
||||
mastoEmoji, err := c.EmojiToMasto(gtsEmoji)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err)
|
||||
}
|
||||
mastoEmojis = append(mastoEmojis, mastoEmoji)
|
||||
}
|
||||
// the status doesn't have gts emojis on it, but it does have emoji IDs
|
||||
// in this case, we need to pull the gts emojis from the db to convert them into masto ones
|
||||
} else {
|
||||
for _, e := range s.Emojis {
|
||||
gtsEmoji := >smodel.Emoji{}
|
||||
if err := c.db.GetByID(e, gtsEmoji); err != nil {
|
||||
return nil, fmt.Errorf("error getting emoji with id %s: %s", e, err)
|
||||
}
|
||||
mastoEmoji, err := c.EmojiToMasto(gtsEmoji)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error converting emoji with id %s: %s", gtsEmoji.ID, err)
|
||||
}
|
||||
mastoEmojis = append(mastoEmojis, mastoEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
var mastoCard *mastotypes.Card
|
||||
var mastoPoll *mastotypes.Poll
|
||||
|
||||
return &mastotypes.Status{
|
||||
ID: s.ID,
|
||||
CreatedAt: s.CreatedAt.Format(time.RFC3339),
|
||||
InReplyToID: s.InReplyToID,
|
||||
InReplyToAccountID: s.InReplyToAccountID,
|
||||
Sensitive: s.Sensitive,
|
||||
SpoilerText: s.ContentWarning,
|
||||
Visibility: util.ParseMastoVisFromGTSVis(s.Visibility),
|
||||
Language: s.Language,
|
||||
URI: s.URI,
|
||||
URL: s.URL,
|
||||
RepliesCount: repliesCount,
|
||||
ReblogsCount: reblogsCount,
|
||||
FavouritesCount: favesCount,
|
||||
Favourited: faved,
|
||||
Reblogged: reblogged,
|
||||
Muted: muted,
|
||||
Bookmarked: bookmarked,
|
||||
Pinned: pinned,
|
||||
Content: s.Content,
|
||||
Reblog: mastoRebloggedStatus,
|
||||
Application: mastoApplication,
|
||||
Account: mastoTargetAccount,
|
||||
MediaAttachments: mastoAttachments,
|
||||
Mentions: mastoMentions,
|
||||
Tags: mastoTags,
|
||||
Emojis: mastoEmojis,
|
||||
Card: mastoCard, // TODO: implement cards
|
||||
Poll: mastoPoll, // TODO: implement polls
|
||||
Text: s.Text,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ type Emoji struct {
|
|||
// EmojiCreateRequest represents a request to create a custom emoji made through the admin API.
|
||||
type EmojiCreateRequest struct {
|
||||
// Desired shortcode for the emoji, without surrounding colons. This must be unique for the domain.
|
||||
Shortcode string `form:"shortcode" validation:"required"`
|
||||
Shortcode string `form:"shortcode" validation:"required"`
|
||||
// Image file to use for the emoji. Must be png or gif and no larger than 50kb.
|
||||
Image *multipart.FileHeader `form:"image" validation:"required"`
|
||||
Image *multipart.FileHeader `form:"image" validation:"required"`
|
||||
}
|
||||
|
|
|
@ -20,8 +20,8 @@ package mastotypes
|
|||
|
||||
// Tag represents a hashtag used within the content of a status. See https://docs.joinmastodon.org/entities/tag/
|
||||
type Tag struct {
|
||||
// The value of the hashtag after the # sign.
|
||||
Name string `json:"name"`
|
||||
// A link to the hashtag on the instance.
|
||||
URL string `json:"url"`
|
||||
// The value of the hashtag after the # sign.
|
||||
Name string `json:"name"`
|
||||
// A link to the hashtag on the instance.
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
|
|
@ -34,19 +34,19 @@ import (
|
|||
|
||||
const (
|
||||
// Key for small/thumbnail versions of media
|
||||
MediaSmall = "small"
|
||||
MediaSmall = "small"
|
||||
// Key for original/fullsize versions of media and emoji
|
||||
MediaOriginal = "original"
|
||||
MediaOriginal = "original"
|
||||
// Key for static (non-animated) versions of emoji
|
||||
MediaStatic = "static"
|
||||
MediaStatic = "static"
|
||||
// Key for media attachments
|
||||
MediaAttachment = "attachment"
|
||||
// Key for profile header
|
||||
MediaHeader = "header"
|
||||
MediaHeader = "header"
|
||||
// Key for profile avatar
|
||||
MediaAvatar = "avatar"
|
||||
MediaAvatar = "avatar"
|
||||
// Key for emoji type
|
||||
MediaEmoji = "emoji"
|
||||
MediaEmoji = "emoji"
|
||||
|
||||
// Maximum permitted bytes of an emoji upload (50kb)
|
||||
EmojiMaxBytes = 51200
|
||||
|
|
|
@ -38,6 +38,10 @@ var testModels []interface{} = []interface{}{
|
|||
>smodel.MediaAttachment{},
|
||||
>smodel.Mention{},
|
||||
>smodel.Status{},
|
||||
>smodel.StatusFave{},
|
||||
>smodel.StatusBookmark{},
|
||||
>smodel.StatusMute{},
|
||||
>smodel.StatusPin{},
|
||||
>smodel.Tag{},
|
||||
>smodel.User{},
|
||||
>smodel.Emoji{},
|
||||
|
|
Loading…
Reference in New Issue