mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-11-11 22:37:30 -06:00
phew
This commit is contained in:
parent
82437a768b
commit
ba84852d3e
19 changed files with 999 additions and 262 deletions
|
|
@ -37,7 +37,7 @@ const (
|
||||||
mediaSizeKey = "media_size"
|
mediaSizeKey = "media_size"
|
||||||
fileNameKey = "file_name"
|
fileNameKey = "file_name"
|
||||||
|
|
||||||
filesPath = "files"
|
filesPath = "files"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fileServer implements the RESTAPIModule interface.
|
// fileServer implements the RESTAPIModule interface.
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ import (
|
||||||
// module implements the apiclient interface
|
// module implements the apiclient interface
|
||||||
type module struct {
|
type module struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
log *logrus.Logger
|
log *logrus.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new security module
|
// New returns a new security module
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ package status
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
"github.com/superseriousbusiness/gotosocial/internal/apimodule"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"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
|
// Route attaches all routes from this module to the given router
|
||||||
func (m *statusModule) Route(r router.Router) error {
|
func (m *statusModule) Route(r router.Router) error {
|
||||||
r.AttachHandler(http.MethodPost, basePath, m.statusCreatePOSTHandler)
|
r.AttachHandler(http.MethodPost, basePath, m.statusCreatePOSTHandler)
|
||||||
// r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler)
|
r.AttachHandler(http.MethodGet, basePathWithID, m.muxHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,6 +91,10 @@ func (m *statusModule) CreateTables(db db.DB) error {
|
||||||
>smodel.Follow{},
|
>smodel.Follow{},
|
||||||
>smodel.FollowRequest{},
|
>smodel.FollowRequest{},
|
||||||
>smodel.Status{},
|
>smodel.Status{},
|
||||||
|
>smodel.StatusFave{},
|
||||||
|
>smodel.StatusBookmark{},
|
||||||
|
>smodel.StatusMute{},
|
||||||
|
>smodel.StatusPin{},
|
||||||
>smodel.Application{},
|
>smodel.Application{},
|
||||||
>smodel.EmailDomainBlock{},
|
>smodel.EmailDomainBlock{},
|
||||||
>smodel.MediaAttachment{},
|
>smodel.MediaAttachment{},
|
||||||
|
|
@ -105,13 +111,14 @@ func (m *statusModule) CreateTables(db db.DB) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (m *statusModule) muxHandler(c *gin.Context) {
|
func (m *statusModule) muxHandler(c *gin.Context) {
|
||||||
// ru := c.Request.RequestURI
|
m.log.Debug("entering mux handler")
|
||||||
// if strings.HasPrefix(ru, verifyPath) {
|
ru := c.Request.RequestURI
|
||||||
// m.accountVerifyGETHandler(c)
|
if strings.HasPrefix(ru, contextPath) {
|
||||||
// } else if strings.HasPrefix(ru, updateCredentialsPath) {
|
// TODO
|
||||||
// m.accountUpdateCredentialsPATCHHandler(c)
|
} else if strings.HasPrefix(ru, rebloggedPath) {
|
||||||
// } else {
|
// TODO
|
||||||
// m.accountGETHandler(c)
|
} else {
|
||||||
// }
|
m.statusGETHandler(c)
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,18 +97,20 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
|
||||||
thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
|
thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
|
||||||
thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
|
thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
|
||||||
newStatus := >smodel.Status{
|
newStatus := >smodel.Status{
|
||||||
ID: thisStatusID,
|
ID: thisStatusID,
|
||||||
URI: thisStatusURI,
|
URI: thisStatusURI,
|
||||||
URL: thisStatusURL,
|
URL: thisStatusURL,
|
||||||
Content: util.HTMLFormat(form.Status),
|
Content: util.HTMLFormat(form.Status),
|
||||||
CreatedAt: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
Local: true,
|
Local: true,
|
||||||
AccountID: authed.Account.ID,
|
AccountID: authed.Account.ID,
|
||||||
ContentWarning: form.SpoilerText,
|
ContentWarning: form.SpoilerText,
|
||||||
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||||
Sensitive: form.Sensitive,
|
Sensitive: form.Sensitive,
|
||||||
Language: form.Language,
|
Language: form.Language,
|
||||||
|
CreatedWithApplicationID: authed.Application.ID,
|
||||||
|
Text: form.Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if replyToID is ok
|
// 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
|
FROM THIS POINT ONWARDS WE ARE JUST CREATING THE FRONTEND REPRESENTATION OF THE STATUS TO RETURN TO THE SUBMITTER
|
||||||
*/
|
*/
|
||||||
mastoVis := util.ParseMastoVisFromGTSVis(newStatus.Visibility)
|
mastoStatus, err := m.mastoConverter.StatusToMasto(newStatus, authed.Account, authed.Account, nil, newStatus.GTSReplyToAccount, newStatus.GTSReplyToStatus)
|
||||||
|
|
||||||
mastoAccount, err := m.mastoConverter.AccountToMastoPublic(authed.Account)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
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 {
|
if err := m.db.Put(menchie); err != nil {
|
||||||
return fmt.Errorf("error putting mentions in db: %s", err)
|
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
|
// add full populated gts menchies to the status for passing them around conveniently
|
||||||
status.GTSMentions = gtsMenchies
|
status.GTSMentions = gtsMenchies
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() {
|
||||||
assert.Len(suite.T(), statusReply.Tags, 1)
|
assert.Len(suite.T(), statusReply.Tags, 1)
|
||||||
assert.Equal(suite.T(), mastomodel.Tag{
|
assert.Equal(suite.T(), mastomodel.Tag{
|
||||||
Name: "helloworld",
|
Name: "helloworld",
|
||||||
URL: "http://localhost:8080/tags/helloworld",
|
URL: "http://localhost:8080/tags/helloworld",
|
||||||
}, statusReply.Tags[0])
|
}, statusReply.Tags[0])
|
||||||
|
|
||||||
gtsTag := >smodel.Tag{}
|
gtsTag := >smodel.Tag{}
|
||||||
|
|
@ -185,7 +185,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() {
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", basePath), nil) // the endpoint we're hitting
|
||||||
ctx.Request.Form = url.Values{
|
ctx.Request.Form = url.Values{
|
||||||
"status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
|
"status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "},
|
||||||
}
|
}
|
||||||
suite.statusModule.statusCreatePOSTHandler(ctx)
|
suite.statusModule.statusCreatePOSTHandler(ctx)
|
||||||
|
|
||||||
|
|
|
||||||
111
internal/apimodule/status/statusget.go
Normal file
111
internal/apimodule/status/statusget.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
167
internal/apimodule/status/statusget_test.go
Normal file
167
internal/apimodule/status/statusget_test.go
Normal file
|
|
@ -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.
|
// That is, it returns true if account1 blocks account2, OR if account2 blocks account1.
|
||||||
Blocked(account1 string, account2 string) (bool, error)
|
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
|
USEFUL CONVERSION FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ type Status struct {
|
||||||
Attachments []string `pg:",array"`
|
Attachments []string `pg:",array"`
|
||||||
// Database IDs of any tags used in this status
|
// Database IDs of any tags used in this status
|
||||||
Tags []string `pg:",array"`
|
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"`
|
Mentions []string `pg:",array"`
|
||||||
// Database IDs of any emojis used in this status
|
// Database IDs of any emojis used in this status
|
||||||
Emojis []string `pg:",array"`
|
Emojis []string `pg:",array"`
|
||||||
|
|
@ -60,11 +60,15 @@ type Status struct {
|
||||||
Sensitive bool
|
Sensitive bool
|
||||||
// what language is this status written in?
|
// what language is this status written in?
|
||||||
Language string
|
Language string
|
||||||
|
// Which application was used to create this status?
|
||||||
|
CreatedWithApplicationID string
|
||||||
// advanced visibility for this status
|
// advanced visibility for this status
|
||||||
VisibilityAdvanced *VisibilityAdvanced
|
VisibilityAdvanced *VisibilityAdvanced
|
||||||
// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
// 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!.
|
// Will probably almost always be Note but who knows!.
|
||||||
ActivityStreamsType ActivityStreamsObject
|
ActivityStreamsType ActivityStreamsObject
|
||||||
|
// Original text of the status without formatting
|
||||||
|
Text string
|
||||||
|
|
||||||
/*
|
/*
|
||||||
NON-DATABASE FIELDS
|
NON-DATABASE FIELDS
|
||||||
|
|
@ -105,6 +109,7 @@ const (
|
||||||
VisibilityDefault Visibility = "public"
|
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 {
|
type VisibilityAdvanced struct {
|
||||||
/*
|
/*
|
||||||
ADVANCED SETTINGS -- These should all default to TRUE.
|
ADVANCED SETTINGS -- These should all default to TRUE.
|
||||||
|
|
@ -123,3 +128,11 @@ type VisibilityAdvanced struct {
|
||||||
// This status can be liked/faved
|
// This status can be liked/faved
|
||||||
Likeable bool `pg:"default:true"`
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
35
internal/db/gtsmodel/statusbookmark.go
Normal file
35
internal/db/gtsmodel/statusbookmark.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
35
internal/db/gtsmodel/statusfave.go
Normal file
35
internal/db/gtsmodel/statusfave.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
35
internal/db/gtsmodel/statusmute.go
Normal file
35
internal/db/gtsmodel/statusmute.go
Normal file
|
|
@ -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"`
|
||||||
|
}
|
||||||
33
internal/db/gtsmodel/statuspin.go
Normal file
33
internal/db/gtsmodel/statuspin.go
Normal file
|
|
@ -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/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
|
||||||
mastotypes "github.com/superseriousbusiness/gotosocial/internal/mastotypes/mastomodel"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
@ -556,160 +555,266 @@ func (ps *postgresService) Blocked(account1 string, account2 string) (bool, erro
|
||||||
return blocked, nil
|
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
|
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) {
|
func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, originAccountID string, statusID string) ([]*gtsmodel.Mention, error) {
|
||||||
menchies := []*gtsmodel.Mention{}
|
menchies := []*gtsmodel.Mention{}
|
||||||
for _, a := range targetAccounts {
|
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 converts a gts model tag into its mastodon (frontend) representation for serialization on the API.
|
||||||
TagToMasto(t *gtsmodel.Tag) (mastotypes.Tag, error)
|
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 {
|
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 ¯\_(ツ)_/¯
|
URL: tagURL, // we don't serve URLs with collections of tagged statuses (FOR NOW) so this is purely for mastodon compatibility ¯\_(ツ)_/¯
|
||||||
}, nil
|
}, 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.
|
// EmojiCreateRequest represents a request to create a custom emoji made through the admin API.
|
||||||
type EmojiCreateRequest struct {
|
type EmojiCreateRequest struct {
|
||||||
// Desired shortcode for the emoji, without surrounding colons. This must be unique for the domain.
|
// 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 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/
|
// Tag represents a hashtag used within the content of a status. See https://docs.joinmastodon.org/entities/tag/
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
// The value of the hashtag after the # sign.
|
// The value of the hashtag after the # sign.
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// A link to the hashtag on the instance.
|
// A link to the hashtag on the instance.
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,19 +34,19 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Key for small/thumbnail versions of media
|
// Key for small/thumbnail versions of media
|
||||||
MediaSmall = "small"
|
MediaSmall = "small"
|
||||||
// Key for original/fullsize versions of media and emoji
|
// Key for original/fullsize versions of media and emoji
|
||||||
MediaOriginal = "original"
|
MediaOriginal = "original"
|
||||||
// Key for static (non-animated) versions of emoji
|
// Key for static (non-animated) versions of emoji
|
||||||
MediaStatic = "static"
|
MediaStatic = "static"
|
||||||
// Key for media attachments
|
// Key for media attachments
|
||||||
MediaAttachment = "attachment"
|
MediaAttachment = "attachment"
|
||||||
// Key for profile header
|
// Key for profile header
|
||||||
MediaHeader = "header"
|
MediaHeader = "header"
|
||||||
// Key for profile avatar
|
// Key for profile avatar
|
||||||
MediaAvatar = "avatar"
|
MediaAvatar = "avatar"
|
||||||
// Key for emoji type
|
// Key for emoji type
|
||||||
MediaEmoji = "emoji"
|
MediaEmoji = "emoji"
|
||||||
|
|
||||||
// Maximum permitted bytes of an emoji upload (50kb)
|
// Maximum permitted bytes of an emoji upload (50kb)
|
||||||
EmojiMaxBytes = 51200
|
EmojiMaxBytes = 51200
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ var testModels []interface{} = []interface{}{
|
||||||
>smodel.MediaAttachment{},
|
>smodel.MediaAttachment{},
|
||||||
>smodel.Mention{},
|
>smodel.Mention{},
|
||||||
>smodel.Status{},
|
>smodel.Status{},
|
||||||
|
>smodel.StatusFave{},
|
||||||
|
>smodel.StatusBookmark{},
|
||||||
|
>smodel.StatusMute{},
|
||||||
|
>smodel.StatusPin{},
|
||||||
>smodel.Tag{},
|
>smodel.Tag{},
|
||||||
>smodel.User{},
|
>smodel.User{},
|
||||||
>smodel.Emoji{},
|
>smodel.Emoji{},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue