This commit is contained in:
tsmethurst 2021-04-17 20:36:33 +02:00
commit ba84852d3e
19 changed files with 999 additions and 262 deletions

View file

@ -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 {
&gtsmodel.Follow{}, &gtsmodel.Follow{},
&gtsmodel.FollowRequest{}, &gtsmodel.FollowRequest{},
&gtsmodel.Status{}, &gtsmodel.Status{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
&gtsmodel.StatusMute{},
&gtsmodel.StatusPin{},
&gtsmodel.Application{}, &gtsmodel.Application{},
&gtsmodel.EmailDomainBlock{}, &gtsmodel.EmailDomainBlock{},
&gtsmodel.MediaAttachment{}, &gtsmodel.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)
// } }
}

View file

@ -109,6 +109,8 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) {
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

View 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 := &gtsmodel.Status{}
if err := m.db.GetByID(targetStatusID, targetStatus); err != nil {
l.Errorf("error fetching status %s: %s", targetStatusID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
l.Tracef("going to search for target account %s", targetStatus.AccountID)
targetAccount := &gtsmodel.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 = &gtsmodel.Status{}
if err := m.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
l.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("status %s not found", targetStatusID)})
return
}
}
mastoStatus, err := m.mastoConverter.StatusToMasto(targetStatus, 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)
}

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

View file

@ -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
*/ */

View file

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

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View file

@ -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 := &gtsmodel.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 := &gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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 := &gtsmodel.RelevantAccounts{
MentionedAccounts: []*gtsmodel.Account{},
}
// get the replied to account from the status and add it to the pile
if targetStatus.InReplyToAccountID != "" {
repliedToAccount := &gtsmodel.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 := &gtsmodel.Status{}
if err := ps.conn.Model(boostedStatus).Where("id = ?", targetStatus.BoostOfID).Select(); err != nil {
return accounts, err
}
boostedAccount := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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(&gtsmodel.Status{}).Where("in_reply_to_id = ?", status.ID).Count()
}
func (ps *postgresService) GetReblogCountForStatus(status *gtsmodel.Status) (int, error) {
return ps.conn.Model(&gtsmodel.Status{}).Where("boost_of_id = ?", status.ID).Count()
}
func (ps *postgresService) GetFaveCountForStatus(status *gtsmodel.Status) (int, error) {
return ps.conn.Model(&gtsmodel.StatusFave{}).Where("status_id = ?", status.ID).Count()
}
func (ps *postgresService) StatusFavedBy(status *gtsmodel.Status, accountID string) (bool, error) {
return ps.conn.Model(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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(&gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 {

View file

@ -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 := &gtsmodel.Application{}
if err := c.db.GetByID(s.CreatedWithApplicationID, application); err != nil {
return nil, fmt.Errorf("error fetching application used to create status: %s", err)
}
mastoApplication, err := c.AppToMastoPublic(application)
if err != nil {
return nil, fmt.Errorf("error parsing application used to create status: %s", err)
}
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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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
}

View file

@ -38,6 +38,10 @@ var testModels []interface{} = []interface{}{
&gtsmodel.MediaAttachment{}, &gtsmodel.MediaAttachment{},
&gtsmodel.Mention{}, &gtsmodel.Mention{},
&gtsmodel.Status{}, &gtsmodel.Status{},
&gtsmodel.StatusFave{},
&gtsmodel.StatusBookmark{},
&gtsmodel.StatusMute{},
&gtsmodel.StatusPin{},
&gtsmodel.Tag{}, &gtsmodel.Tag{},
&gtsmodel.User{}, &gtsmodel.User{},
&gtsmodel.Emoji{}, &gtsmodel.Emoji{},