mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
Timeline manager (#40)
* start messing about with timeline manager * i have no idea what i'm doing * i continue to not know what i'm doing * it's coming along * bit more progress * update timeline with new posts as they come in * lint and fmt * Select accounts where empty string * restructure a bunch, get unfaves working * moving stuff around * federate status deletes properly * mention regex better but not 100% there * fix regex * some more hacking away at the timeline code phew * fix up some little things * i can't even * more timeline stuff * move to ulid * fiddley * some lil fixes for kibou compatibility * timelines working pretty alright! * tidy + lint
This commit is contained in:
parent
6ac6f8d614
commit
b4288f3c47
1
go.mod
1
go.mod
|
@ -33,6 +33,7 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
|
github.com/oklog/ulid v1.3.1
|
||||||
github.com/onsi/gomega v1.13.0 // indirect
|
github.com/onsi/gomega v1.13.0 // indirect
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -204,6 +204,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||||
|
|
|
@ -17,372 +17,3 @@
|
||||||
// */
|
// */
|
||||||
|
|
||||||
package account_test
|
package account_test
|
||||||
|
|
||||||
// import (
|
|
||||||
// "bytes"
|
|
||||||
// "encoding/json"
|
|
||||||
// "fmt"
|
|
||||||
// "io"
|
|
||||||
// "io/ioutil"
|
|
||||||
// "mime/multipart"
|
|
||||||
// "net/http"
|
|
||||||
// "net/http/httptest"
|
|
||||||
// "os"
|
|
||||||
// "testing"
|
|
||||||
|
|
||||||
// "github.com/gin-gonic/gin"
|
|
||||||
// "github.com/google/uuid"
|
|
||||||
// "github.com/stretchr/testify/assert"
|
|
||||||
// "github.com/stretchr/testify/suite"
|
|
||||||
// "github.com/superseriousbusiness/gotosocial/internal/api/client/account"
|
|
||||||
// "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
|
||||||
// "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
// "github.com/superseriousbusiness/gotosocial/testrig"
|
|
||||||
|
|
||||||
// "github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
// "golang.org/x/crypto/bcrypt"
|
|
||||||
// )
|
|
||||||
|
|
||||||
// type AccountCreateTestSuite struct {
|
|
||||||
// AccountStandardTestSuite
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (suite *AccountCreateTestSuite) SetupSuite() {
|
|
||||||
// suite.testTokens = testrig.NewTestTokens()
|
|
||||||
// suite.testClients = testrig.NewTestClients()
|
|
||||||
// suite.testApplications = testrig.NewTestApplications()
|
|
||||||
// suite.testUsers = testrig.NewTestUsers()
|
|
||||||
// suite.testAccounts = testrig.NewTestAccounts()
|
|
||||||
// suite.testAttachments = testrig.NewTestAttachments()
|
|
||||||
// suite.testStatuses = testrig.NewTestStatuses()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (suite *AccountCreateTestSuite) SetupTest() {
|
|
||||||
// suite.config = testrig.NewTestConfig()
|
|
||||||
// suite.db = testrig.NewTestDB()
|
|
||||||
// suite.storage = testrig.NewTestStorage()
|
|
||||||
// suite.log = testrig.NewTestLog()
|
|
||||||
// suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)))
|
|
||||||
// suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator)
|
|
||||||
// suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module)
|
|
||||||
// testrig.StandardDBSetup(suite.db)
|
|
||||||
// testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func (suite *AccountCreateTestSuite) TearDownTest() {
|
|
||||||
// testrig.StandardDBTeardown(suite.db)
|
|
||||||
// testrig.StandardStorageTeardown(suite.storage)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerSuccessful checks the happy path for an account creation request: all the fields provided are valid,
|
|
||||||
// // and at the end of it a new user and account should be added into the database.
|
|
||||||
// //
|
|
||||||
// // This is the handler served at /api/v1/accounts as POST
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() {
|
|
||||||
|
|
||||||
// t := suite.testTokens["local_account_1"]
|
|
||||||
// oauthToken := oauth.TokenToOauthToken(t)
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
|
|
||||||
// // 1. we should have OK from our call to the function
|
|
||||||
// suite.EqualValues(http.StatusOK, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have a token in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// t := &model.Token{}
|
|
||||||
// err = json.Unmarshal(b, t)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), "we're authorized now!", t.AccessToken)
|
|
||||||
|
|
||||||
// // check new account
|
|
||||||
|
|
||||||
// // 1. we should be able to get the new account from the db
|
|
||||||
// acct := >smodel.Account{}
|
|
||||||
// err = suite.db.GetLocalAccountByUsername("test_user", acct)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.NotNil(suite.T(), acct)
|
|
||||||
// // 2. reason should be set
|
|
||||||
// assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("reason"), acct.Reason)
|
|
||||||
// // 3. display name should be equal to username by default
|
|
||||||
// assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("username"), acct.DisplayName)
|
|
||||||
// // 4. domain should be nil because this is a local account
|
|
||||||
// assert.Nil(suite.T(), nil, acct.Domain)
|
|
||||||
// // 5. id should be set and parseable as a uuid
|
|
||||||
// assert.NotNil(suite.T(), acct.ID)
|
|
||||||
// _, err = uuid.Parse(acct.ID)
|
|
||||||
// assert.Nil(suite.T(), err)
|
|
||||||
// // 6. private and public key should be set
|
|
||||||
// assert.NotNil(suite.T(), acct.PrivateKey)
|
|
||||||
// assert.NotNil(suite.T(), acct.PublicKey)
|
|
||||||
|
|
||||||
// // check new user
|
|
||||||
|
|
||||||
// // 1. we should be able to get the new user from the db
|
|
||||||
// usr := >smodel.User{}
|
|
||||||
// err = suite.db.GetWhere("unconfirmed_email", suite.newUserFormHappyPath.Get("email"), usr)
|
|
||||||
// assert.Nil(suite.T(), err)
|
|
||||||
// assert.NotNil(suite.T(), usr)
|
|
||||||
|
|
||||||
// // 2. user should have account id set to account we got above
|
|
||||||
// assert.Equal(suite.T(), acct.ID, usr.AccountID)
|
|
||||||
|
|
||||||
// // 3. id should be set and parseable as a uuid
|
|
||||||
// assert.NotNil(suite.T(), usr.ID)
|
|
||||||
// _, err = uuid.Parse(usr.ID)
|
|
||||||
// assert.Nil(suite.T(), err)
|
|
||||||
|
|
||||||
// // 4. locale should be equal to what we requested
|
|
||||||
// assert.Equal(suite.T(), suite.newUserFormHappyPath.Get("locale"), usr.Locale)
|
|
||||||
|
|
||||||
// // 5. created by application id should be equal to the app id
|
|
||||||
// assert.Equal(suite.T(), suite.testApplication.ID, usr.CreatedByApplicationID)
|
|
||||||
|
|
||||||
// // 6. password should be matcheable to what we set above
|
|
||||||
// err = bcrypt.CompareHashAndPassword([]byte(usr.EncryptedPassword), []byte(suite.newUserFormHappyPath.Get("password")))
|
|
||||||
// assert.Nil(suite.T(), err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no authorization is provided:
|
|
||||||
// // only registered applications can create accounts, and we don't provide one here.
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoAuth() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
|
|
||||||
// // 1. we should have forbidden from our call to the function because we didn't auth
|
|
||||||
// suite.EqualValues(http.StatusForbidden, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerNoAuth makes sure that the handler fails when no form is provided at all.
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerNoForm() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"missing one or more required form values"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerWeakPassword makes sure that the handler fails when a weak password is provided
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeakPassword() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
// // set a weak password
|
|
||||||
// ctx.Request.Form.Set("password", "weak")
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"insecure password, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerWeirdLocale makes sure that the handler fails when a weird locale is provided
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerWeirdLocale() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
// // set an invalid locale
|
|
||||||
// ctx.Request.Form.Set("locale", "neverneverland")
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"language: tag is not well-formed"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerRegistrationsClosed makes sure that the handler fails when registrations are closed
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerRegistrationsClosed() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
|
|
||||||
// // close registrations
|
|
||||||
// suite.config.AccountsConfig.OpenRegistration = false
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"registration is not open for this server"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when no reason is provided but one is required
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerReasonNotProvided() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
|
|
||||||
// // remove reason
|
|
||||||
// ctx.Request.Form.Set("reason", "")
|
|
||||||
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"no reason provided"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TestAccountCreatePOSTHandlerReasonNotProvided makes sure that the handler fails when a crappy reason is presented but a good one is required
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerInsufficientReason() {
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplication)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", account.BasePath), nil) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Form = suite.newUserFormHappyPath
|
|
||||||
|
|
||||||
// // remove reason
|
|
||||||
// ctx.Request.Form.Set("reason", "just cuz")
|
|
||||||
|
|
||||||
// suite.accountModule.AccountCreatePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
// suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// assert.Equal(suite.T(), `{"error":"reason should be at least 40 chars but 'just cuz' was 8"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// /*
|
|
||||||
// TESTING: AccountUpdateCredentialsPATCHHandler
|
|
||||||
// */
|
|
||||||
|
|
||||||
// func (suite *AccountCreateTestSuite) TestAccountUpdateCredentialsPATCHHandler() {
|
|
||||||
|
|
||||||
// // put test local account in db
|
|
||||||
// err := suite.db.Put(suite.testAccountLocal)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
// // attach avatar to request
|
|
||||||
// aviFile, err := os.Open("../../media/test/test-jpeg.jpg")
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
// body := &bytes.Buffer{}
|
|
||||||
// writer := multipart.NewWriter(body)
|
|
||||||
|
|
||||||
// part, err := writer.CreateFormFile("avatar", "test-jpeg.jpg")
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
// _, err = io.Copy(part, aviFile)
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
// err = aviFile.Close()
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
// err = writer.Close()
|
|
||||||
// assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
// // setup
|
|
||||||
// recorder := httptest.NewRecorder()
|
|
||||||
// ctx, _ := gin.CreateTestContext(recorder)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccountLocal)
|
|
||||||
// ctx.Set(oauth.SessionAuthorizedToken, suite.testToken)
|
|
||||||
// ctx.Request = httptest.NewRequest(http.MethodPatch, fmt.Sprintf("http://localhost:8080/%s", account.UpdateCredentialsPath), body) // the endpoint we're hitting
|
|
||||||
// ctx.Request.Header.Set("Content-Type", writer.FormDataContentType())
|
|
||||||
// suite.accountModule.AccountUpdateCredentialsPATCHHandler(ctx)
|
|
||||||
|
|
||||||
// // check response
|
|
||||||
|
|
||||||
// // 1. we should have OK because our request was valid
|
|
||||||
// suite.EqualValues(http.StatusOK, recorder.Code)
|
|
||||||
|
|
||||||
// // 2. we should have an error message in the result body
|
|
||||||
// result := recorder.Result()
|
|
||||||
// defer result.Body.Close()
|
|
||||||
// // TODO: implement proper checks here
|
|
||||||
// //
|
|
||||||
// // b, err := ioutil.ReadAll(result.Body)
|
|
||||||
// // assert.NoError(suite.T(), err)
|
|
||||||
// // assert.Equal(suite.T(), `{"error":"not authorized"}`, string(b))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// func TestAccountCreateTestSuite(t *testing.T) {
|
|
||||||
// suite.Run(t, new(AccountCreateTestSuite))
|
|
||||||
// }
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
||||||
|
|
||||||
// ValidatePassword takes an email address and a password.
|
// ValidatePassword takes an email address and a password.
|
||||||
// The goal is to authenticate the password against the one for that email
|
// The goal is to authenticate the password against the one for that email
|
||||||
// address stored in the database. If OK, we return the userid (a uuid) for that user,
|
// address stored in the database. If OK, we return the userid (a ulid) for that user,
|
||||||
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
|
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
|
||||||
func (m *Module) ValidatePassword(email string, password string) (userid string, err error) {
|
func (m *Module) ValidatePassword(email string, password string) (userid string, err error) {
|
||||||
l := m.log.WithField("func", "ValidatePassword")
|
l := m.log.WithField("func", "ValidatePassword")
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package emoji
|
package emoji
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
// EmojisGETHandler returns a list of custom emojis enabled on the instance
|
// EmojisGETHandler returns a list of custom emojis enabled on the instance
|
||||||
func (m *Module) EmojisGETHandler(c *gin.Context) {
|
func (m *Module) EmojisGETHandler(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, []string{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// AccountIDKey is the url key for account id (an account uuid)
|
// AccountIDKey is the url key for account id (an account ulid)
|
||||||
AccountIDKey = "account_id"
|
AccountIDKey = "account_id"
|
||||||
// MediaTypeKey is the url key for media type (usually something like attachment or header etc)
|
// MediaTypeKey is the url key for media type (usually something like attachment or header etc)
|
||||||
MediaTypeKey = "media_type"
|
MediaTypeKey = "media_type"
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package filter
|
package filter
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
// FiltersGETHandler returns a list of filters set by/for the authed account
|
// FiltersGETHandler returns a list of filters set by/for the authed account
|
||||||
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, []string{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package list
|
package list
|
||||||
|
|
||||||
import "github.com/gin-gonic/gin"
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
// ListsGETHandler returns a list of lists created by/for the authed account
|
// ListsGETHandler returns a list of lists created by/for the authed account
|
||||||
func (m *Module) ListsGETHandler(c *gin.Context) {
|
func (m *Module) ListsGETHandler(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, []string{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,5 +56,11 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the status was already gone/never existed
|
||||||
|
if mastoStatus == nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, mastoStatus)
|
c.JSON(http.StatusOK, mastoStatus)
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,12 +87,13 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||||
local = i
|
local = i
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses, errWithCode := m.processor.HomeTimelineGet(authed, maxID, sinceID, minID, limit, local)
|
resp, errWithCode := m.processor.HomeTimelineGet(authed, maxID, sinceID, minID, limit, local)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
l.Debugf("error from processor HomeTimelineGet: %s", errWithCode)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, statuses)
|
c.Header("Link", resp.LinkHeader)
|
||||||
|
c.JSON(http.StatusOK, resp.Statuses)
|
||||||
}
|
}
|
||||||
|
|
8
internal/api/model/timeline.go
Normal file
8
internal/api/model/timeline.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link
|
||||||
|
// header for the previous and next queries, to be returned to the client.
|
||||||
|
type StatusTimelineResponse struct {
|
||||||
|
Statuses []*Status
|
||||||
|
LinkHeader string
|
||||||
|
}
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
||||||
|
@ -42,17 +42,18 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
||||||
|
|
||||||
posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request)
|
posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if withCode, ok := err.(processing.ErrorWithCode); ok {
|
if withCode, ok := err.(gtserror.WithCode); ok {
|
||||||
l.Debug(withCode.Error())
|
l.Debug(withCode.Error())
|
||||||
c.JSON(withCode.Code(), withCode.Safe())
|
c.JSON(withCode.Code(), withCode.Safe())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Debug(err)
|
l.Debugf("InboxPOSTHandler: error processing request: %s", err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !posted {
|
if !posted {
|
||||||
|
l.Debugf("request could not be handled as an AP request; headers were: %+v", c.Request.Header)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,42 +24,53 @@
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org
|
// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org
|
||||||
func (m *Module) WebfingerGETRequest(c *gin.Context) {
|
func (m *Module) WebfingerGETRequest(c *gin.Context) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "WebfingerGETRequest",
|
||||||
|
"user-agent": c.Request.UserAgent(),
|
||||||
|
})
|
||||||
|
|
||||||
q, set := c.GetQuery("resource")
|
q, set := c.GetQuery("resource")
|
||||||
if !set || q == "" {
|
if !set || q == "" {
|
||||||
|
l.Debug("aborting request because no resource was set in query")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
withAcct := strings.Split(q, "acct:")
|
withAcct := strings.Split(q, "acct:")
|
||||||
if len(withAcct) != 2 {
|
if len(withAcct) != 2 {
|
||||||
|
l.Debugf("aborting request because resource query %s could not be split by 'acct:'", q)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
usernameDomain := strings.Split(withAcct[1], "@")
|
usernameDomain := strings.Split(withAcct[1], "@")
|
||||||
if len(usernameDomain) != 2 {
|
if len(usernameDomain) != 2 {
|
||||||
|
l.Debugf("aborting request because username and domain could not be parsed from %s", withAcct[1])
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username := strings.ToLower(usernameDomain[0])
|
username := strings.ToLower(usernameDomain[0])
|
||||||
domain := strings.ToLower(usernameDomain[1])
|
domain := strings.ToLower(usernameDomain[1])
|
||||||
if username == "" || domain == "" {
|
if username == "" || domain == "" {
|
||||||
|
l.Debug("aborting request because username or domain was empty")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if domain != m.config.Host {
|
if domain != m.config.Host {
|
||||||
|
l.Debugf("aborting request because domain %s does not belong to this instance", domain)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("domain %s does not belong to this instance", domain)})
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("domain %s does not belong to this instance", domain)})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := m.processor.GetWebfingerAccount(username, c.Request)
|
resp, err := m.processor.GetWebfingerAccount(username, c.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
l.Debugf("aborting request with an error: %s", err.Error())
|
||||||
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
c.JSON(err.Code(), gin.H{"error": err.Safe()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
17
internal/api/security/robots.go
Normal file
17
internal/api/security/robots.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const robotsString = `User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
`
|
||||||
|
|
||||||
|
// RobotsGETHandler returns the most restrictive possible robots.txt file in response to a call to /robots.txt.
|
||||||
|
// The response instructs bots with *any* user agent not to index the instance at all.
|
||||||
|
func (m *Module) RobotsGETHandler(c *gin.Context) {
|
||||||
|
c.String(http.StatusOK, robotsString)
|
||||||
|
}
|
|
@ -19,12 +19,16 @@
|
||||||
package security
|
package security
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const robotsPath = "/robots.txt"
|
||||||
|
|
||||||
// Module implements the ClientAPIModule interface for security middleware
|
// Module implements the ClientAPIModule interface for security middleware
|
||||||
type Module struct {
|
type Module struct {
|
||||||
config *config.Config
|
config *config.Config
|
||||||
|
@ -44,5 +48,6 @@ func (m *Module) Route(s router.Router) error {
|
||||||
s.AttachMiddleware(m.FlocBlock)
|
s.AttachMiddleware(m.FlocBlock)
|
||||||
s.AttachMiddleware(m.ExtraHeaders)
|
s.AttachMiddleware(m.ExtraHeaders)
|
||||||
s.AttachMiddleware(m.UserAgentBlock)
|
s.AttachMiddleware(m.UserAgentBlock)
|
||||||
|
s.AttachHandler(http.MethodGet, robotsPath, m.RobotsGETHandler)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,20 +23,24 @@
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserAgentBlock is a middleware that prevents google chrome cohort tracking by
|
// UserAgentBlock blocks requests with undesired, empty, or invalid user-agent strings.
|
||||||
// writing the Permissions-Policy header after all other parts of the request have been completed.
|
|
||||||
// See: https://plausible.io/blog/google-floc
|
|
||||||
func (m *Module) UserAgentBlock(c *gin.Context) {
|
func (m *Module) UserAgentBlock(c *gin.Context) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "UserAgentBlock",
|
||||||
|
})
|
||||||
|
|
||||||
ua := c.Request.UserAgent()
|
ua := c.Request.UserAgent()
|
||||||
if ua == "" {
|
if ua == "" {
|
||||||
|
l.Debug("aborting request because there's no user-agent set")
|
||||||
c.AbortWithStatus(http.StatusTeapot)
|
c.AbortWithStatus(http.StatusTeapot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(c.Request.UserAgent()), strings.ToLower("friendica")) {
|
if strings.Contains(strings.ToLower(ua), strings.ToLower("friendica")) {
|
||||||
|
l.Debugf("aborting request with user-agent %s because it contains 'friendica'", ua)
|
||||||
c.AbortWithStatus(http.StatusTeapot)
|
c.AbortWithStatus(http.StatusTeapot)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
|
timelineprocessing "github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
@ -74,6 +75,20 @@
|
||||||
return fmt.Errorf("error creating dbservice: %s", err)
|
return fmt.Errorf("error creating dbservice: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, m := range models {
|
||||||
|
if err := dbService.CreateTable(m); err != nil {
|
||||||
|
return fmt.Errorf("table creation error: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbService.CreateInstanceAccount(); err != nil {
|
||||||
|
return fmt.Errorf("error creating instance account: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dbService.CreateInstanceInstance(); err != nil {
|
||||||
|
return fmt.Errorf("error creating instance instance: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
federatingDB := federatingdb.New(dbService, c, log)
|
federatingDB := federatingdb.New(dbService, c, log)
|
||||||
|
|
||||||
router, err := router.New(c, log)
|
router, err := router.New(c, log)
|
||||||
|
@ -88,13 +103,14 @@
|
||||||
|
|
||||||
// build converters and util
|
// build converters and util
|
||||||
typeConverter := typeutils.NewConverter(c, dbService)
|
typeConverter := typeutils.NewConverter(c, dbService)
|
||||||
|
timelineManager := timelineprocessing.NewManager(dbService, typeConverter, c, log)
|
||||||
|
|
||||||
// build backend handlers
|
// build backend handlers
|
||||||
mediaHandler := media.New(c, dbService, storageBackend, log)
|
mediaHandler := media.New(c, dbService, storageBackend, log)
|
||||||
oauthServer := oauth.New(dbService, log)
|
oauthServer := oauth.New(dbService, log)
|
||||||
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
|
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
|
||||||
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
federator := federation.NewFederator(dbService, federatingDB, transportController, c, log, typeConverter)
|
||||||
processor := processing.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log)
|
processor := processing.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, timelineManager, dbService, log)
|
||||||
if err := processor.Start(); err != nil {
|
if err := processor.Start(); err != nil {
|
||||||
return fmt.Errorf("error starting processor: %s", err)
|
return fmt.Errorf("error starting processor: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -149,20 +165,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, m := range models {
|
|
||||||
if err := dbService.CreateTable(m); err != nil {
|
|
||||||
return fmt.Errorf("table creation error: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dbService.CreateInstanceAccount(); err != nil {
|
|
||||||
return fmt.Errorf("error creating instance account: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := dbService.CreateInstanceInstance(); err != nil {
|
|
||||||
return fmt.Errorf("error creating instance instance: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gts, err := gotosocial.NewServer(dbService, router, federator, c)
|
gts, err := gotosocial.NewServer(dbService, router, federator, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error creating gotosocial service: %s", err)
|
return fmt.Errorf("error creating gotosocial service: %s", err)
|
||||||
|
|
|
@ -143,7 +143,9 @@ type DB interface {
|
||||||
// GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by.
|
// GetFollowersByAccountID is a shortcut for the common action of fetching a list of accounts that accountID is followed by.
|
||||||
// The given slice 'followers' will be set to the result of the query, whatever it is.
|
// The given slice 'followers' will be set to the result of the query, whatever it is.
|
||||||
// In case of no entries, a 'no entries' error will be returned
|
// In case of no entries, a 'no entries' error will be returned
|
||||||
GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error
|
//
|
||||||
|
// If localOnly is set to true, then only followers from *this instance* will be returned.
|
||||||
|
GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error
|
||||||
|
|
||||||
// GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID.
|
// GetFavesByAccountID is a shortcut for the common action of fetching a list of faves made by the given accountID.
|
||||||
// The given slice 'faves' will be set to the result of the query, whatever it is.
|
// The given slice 'faves' will be set to the result of the query, whatever it is.
|
||||||
|
@ -210,7 +212,7 @@ type DB interface {
|
||||||
// 3. Accounts boosted 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.
|
// 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)
|
StatusVisible(targetStatus *gtsmodel.Status, 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 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)
|
Follows(sourceAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (bool, error)
|
||||||
|
@ -245,10 +247,6 @@ type DB interface {
|
||||||
// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
// StatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
||||||
StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
StatusBookmarkedBy(status *gtsmodel.Status, accountID string) (bool, error)
|
||||||
|
|
||||||
// UnfaveStatus unfaves the given status, using accountID as the unfaver (sure, that's a word).
|
|
||||||
// The returned fave will be nil if the status was already not faved.
|
|
||||||
UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error)
|
|
||||||
|
|
||||||
// WhoFavedStatus returns a slice of accounts who faved the given status.
|
// WhoFavedStatus returns a slice of accounts who faved the given status.
|
||||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||||
WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
||||||
|
@ -257,9 +255,8 @@ type DB interface {
|
||||||
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
// This slice will be unfiltered, not taking account of blocks and whatnot, so filter it before serving it back to a user.
|
||||||
WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error)
|
||||||
|
|
||||||
// GetHomeTimelineForAccount fetches the account's HOME timeline -- ie., posts and replies from people they *follow*.
|
// GetStatusesWhereFollowing returns a slice of statuses from accounts that are followed by the given account id.
|
||||||
// It will use the given filters and try to return as many statuses up to the limit as possible.
|
GetStatusesWhereFollowing(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
||||||
GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error)
|
|
||||||
|
|
||||||
// GetPublicTimelineForAccount fetches the account's PUBLIC timline -- ie., posts and replies that are public.
|
// GetPublicTimelineForAccount fetches the account's PUBLIC timline -- ie., posts and replies that are public.
|
||||||
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
// It will use the given filters and try to return as many statuses as possible up to the limit.
|
||||||
|
|
|
@ -33,11 +33,11 @@
|
||||||
"github.com/go-pg/pg/extra/pgdebug"
|
"github.com/go-pg/pg/extra/pgdebug"
|
||||||
"github.com/go-pg/pg/v10"
|
"github.com/go-pg/pg/v10"
|
||||||
"github.com/go-pg/pg/v10/orm"
|
"github.com/go-pg/pg/v10/orm"
|
||||||
"github.com/google/uuid"
|
|
||||||
"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"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
@ -223,12 +223,16 @@ func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error {
|
||||||
|
|
||||||
q := ps.conn.Model(i)
|
q := ps.conn.Model(i)
|
||||||
for _, w := range where {
|
for _, w := range where {
|
||||||
if w.CaseInsensitive {
|
|
||||||
q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value)
|
|
||||||
} else {
|
|
||||||
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if w.Value == nil {
|
||||||
|
q = q.Where("? IS NULL", pg.Ident(w.Key))
|
||||||
|
} else {
|
||||||
|
if w.CaseInsensitive {
|
||||||
|
q = q.Where("LOWER(?) = LOWER(?)", pg.Safe(w.Key), w.Value)
|
||||||
|
} else {
|
||||||
|
q = q.Where("? = ?", pg.Safe(w.Key), w.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := q.Select(); err != nil {
|
if err := q.Select(); err != nil {
|
||||||
|
@ -240,10 +244,6 @@ func (ps *postgresService) GetWhere(where []db.Where, i interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (ps *postgresService) GetWhereMany(i interface{}, where ...model.Where) error {
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (ps *postgresService) GetAll(i interface{}) error {
|
func (ps *postgresService) GetAll(i interface{}) error {
|
||||||
if err := ps.conn.Model(i).Select(); err != nil {
|
if err := ps.conn.Model(i).Select(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
|
@ -334,6 +334,7 @@ func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAcc
|
||||||
|
|
||||||
// create a new follow to 'replace' the request with
|
// create a new follow to 'replace' the request with
|
||||||
follow := >smodel.Follow{
|
follow := >smodel.Follow{
|
||||||
|
ID: fr.ID,
|
||||||
AccountID: originAccountID,
|
AccountID: originAccountID,
|
||||||
TargetAccountID: targetAccountID,
|
TargetAccountID: targetAccountID,
|
||||||
URI: fr.URI,
|
URI: fr.URI,
|
||||||
|
@ -360,8 +361,14 @@ func (ps *postgresService) CreateInstanceAccount() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||||
a := >smodel.Account{
|
a := >smodel.Account{
|
||||||
|
ID: aID,
|
||||||
Username: ps.config.Host,
|
Username: ps.config.Host,
|
||||||
DisplayName: username,
|
DisplayName: username,
|
||||||
URL: newAccountURIs.UserURL,
|
URL: newAccountURIs.UserURL,
|
||||||
|
@ -389,7 +396,13 @@ func (ps *postgresService) CreateInstanceAccount() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) CreateInstanceInstance() error {
|
func (ps *postgresService) CreateInstanceInstance() error {
|
||||||
|
iID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
i := >smodel.Instance{
|
i := >smodel.Instance{
|
||||||
|
ID: iID,
|
||||||
Domain: ps.config.Host,
|
Domain: ps.config.Host,
|
||||||
Title: ps.config.Host,
|
Title: ps.config.Host,
|
||||||
URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host),
|
URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host),
|
||||||
|
@ -455,8 +468,28 @@ func (ps *postgresService) GetFollowingByAccountID(accountID string, following *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow) error {
|
func (ps *postgresService) GetFollowersByAccountID(accountID string, followers *[]gtsmodel.Follow, localOnly bool) error {
|
||||||
if err := ps.conn.Model(followers).Where("target_account_id = ?", accountID).Select(); err != nil {
|
|
||||||
|
q := ps.conn.Model(followers)
|
||||||
|
|
||||||
|
if localOnly {
|
||||||
|
// for local accounts let's get where domain is null OR where domain is an empty string, just to be safe
|
||||||
|
whereGroup := func(q *pg.Query) (*pg.Query, error) {
|
||||||
|
q = q.
|
||||||
|
WhereOr("? IS NULL", pg.Ident("a.domain")).
|
||||||
|
WhereOr("a.domain = ?", "")
|
||||||
|
return q, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q = q.ColumnExpr("follow.*").
|
||||||
|
Join("JOIN accounts AS a ON follow.account_id = TEXT(a.id)").
|
||||||
|
Where("follow.target_account_id = ?", accountID).
|
||||||
|
WhereGroup(whereGroup)
|
||||||
|
} else {
|
||||||
|
q = q.Where("target_account_id = ?", accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := q.Select(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -580,8 +613,13 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr
|
||||||
}
|
}
|
||||||
|
|
||||||
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host)
|
||||||
|
newAccountID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
a := >smodel.Account{
|
a := >smodel.Account{
|
||||||
|
ID: newAccountID,
|
||||||
Username: username,
|
Username: username,
|
||||||
DisplayName: username,
|
DisplayName: username,
|
||||||
Reason: reason,
|
Reason: reason,
|
||||||
|
@ -605,8 +643,15 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error hashing password: %s", err)
|
return nil, fmt.Errorf("error hashing password: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newUserID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
u := >smodel.User{
|
u := >smodel.User{
|
||||||
AccountID: a.ID,
|
ID: newUserID,
|
||||||
|
AccountID: newAccountID,
|
||||||
EncryptedPassword: string(pw),
|
EncryptedPassword: string(pw),
|
||||||
SignUpIP: signUpIP,
|
SignUpIP: signUpIP,
|
||||||
Locale: locale,
|
Locale: locale,
|
||||||
|
@ -761,12 +806,14 @@ func (ps *postgresService) GetRelationship(requestingAccount string, targetAccou
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account, relevantAccounts *gtsmodel.RelevantAccounts) (bool, error) {
|
||||||
l := ps.log.WithField("func", "StatusVisible")
|
l := ps.log.WithField("func", "StatusVisible")
|
||||||
|
|
||||||
|
targetAccount := relevantAccounts.StatusAuthor
|
||||||
|
|
||||||
// if target account is suspended then don't show the status
|
// if target account is suspended then don't show the status
|
||||||
if !targetAccount.SuspendedAt.IsZero() {
|
if !targetAccount.SuspendedAt.IsZero() {
|
||||||
l.Debug("target account suspended at is not zero")
|
l.Trace("target account suspended at is not zero")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -785,7 +832,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
// if target user is disabled, not yet approved, or not confirmed then don't show the status
|
// 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!)
|
// (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() {
|
if targetUser.Disabled || !targetUser.Approved || targetUser.ConfirmedAt.IsZero() {
|
||||||
l.Debug("target user is disabled, not approved, or not confirmed")
|
l.Trace("target user is disabled, not approved, or not confirmed")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -793,18 +840,17 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
// If requesting account is nil, that means whoever requested the status didn't auth, or their auth failed.
|
// 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.
|
// In this case, we can still serve the status if it's public, otherwise we definitely shouldn't.
|
||||||
if requestingAccount == nil {
|
if requestingAccount == nil {
|
||||||
|
|
||||||
if targetStatus.Visibility == gtsmodel.VisibilityPublic {
|
if targetStatus.Visibility == gtsmodel.VisibilityPublic {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
l.Debug("requesting account is nil but the target status isn't public")
|
l.Trace("requesting account is nil but the target status isn't public")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// if requesting account is suspended then don't show the status -- although they probably shouldn't have gotten
|
// 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.
|
// this far (ie., been authed) in the first place: this is just for safety.
|
||||||
if !requestingAccount.SuspendedAt.IsZero() {
|
if !requestingAccount.SuspendedAt.IsZero() {
|
||||||
l.Debug("requesting account is suspended")
|
l.Trace("requesting account is suspended")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -822,7 +868,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
}
|
}
|
||||||
// okay, user exists, so make sure it has full privileges/is confirmed/approved
|
// okay, user exists, so make sure it has full privileges/is confirmed/approved
|
||||||
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
|
if requestingUser.Disabled || !requestingUser.Approved || requestingUser.ConfirmedAt.IsZero() {
|
||||||
l.Debug("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
|
l.Trace("requesting account is local but corresponding user is either disabled, not approved, or not confirmed")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -839,20 +885,32 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
return false, err
|
return false, err
|
||||||
} else if blocked {
|
} 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
|
// don't allow the status to be viewed if a block exists in *either* direction between these two accounts, no creepy stalking please
|
||||||
l.Debug("a block exists between requesting account and target account")
|
l.Trace("a block exists between requesting account and target account")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check other accounts mentioned/boosted by/replied to by the status, if they exist
|
// check other accounts mentioned/boosted by/replied to by the status, if they exist
|
||||||
if relevantAccounts != nil {
|
if relevantAccounts != nil {
|
||||||
// status replies to account id
|
// status replies to account id
|
||||||
if relevantAccounts.ReplyToAccount != nil {
|
if relevantAccounts.ReplyToAccount != nil && relevantAccounts.ReplyToAccount.ID != requestingAccount.ID {
|
||||||
if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil {
|
if blocked, err := ps.Blocked(relevantAccounts.ReplyToAccount.ID, requestingAccount.ID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
l.Debug("a block exists between requesting account and reply to account")
|
l.Trace("a block exists between requesting account and reply to account")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check reply to ID
|
||||||
|
if targetStatus.InReplyToID != "" {
|
||||||
|
followsRepliedAccount, err := ps.Follows(requestingAccount, relevantAccounts.ReplyToAccount)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !followsRepliedAccount {
|
||||||
|
l.Trace("target status is a followers-only reply to an account that is not followed by the requesting account")
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// status boosts accounts id
|
// status boosts accounts id
|
||||||
|
@ -860,7 +918,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil {
|
if blocked, err := ps.Blocked(relevantAccounts.BoostedAccount.ID, requestingAccount.ID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
l.Debug("a block exists between requesting account and boosted account")
|
l.Trace("a block exists between requesting account and boosted account")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -870,7 +928,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil {
|
if blocked, err := ps.Blocked(relevantAccounts.BoostedReplyToAccount.ID, requestingAccount.ID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
l.Debug("a block exists between requesting account and boosted reply to account")
|
l.Trace("a block exists between requesting account and boosted reply to account")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -880,7 +938,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil {
|
if blocked, err := ps.Blocked(a.ID, requestingAccount.ID); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
} else if blocked {
|
} else if blocked {
|
||||||
l.Debug("a block exists between requesting account and a mentioned account")
|
l.Trace("a block exists between requesting account and a mentioned account")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -906,7 +964,7 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if !follows {
|
if !follows {
|
||||||
l.Debug("requested status is followers only but requesting account is not a follower")
|
l.Trace("requested status is followers only but requesting account is not a follower")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
|
@ -917,12 +975,12 @@ func (ps *postgresService) StatusVisible(targetStatus *gtsmodel.Status, targetAc
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if !mutuals {
|
if !mutuals {
|
||||||
l.Debug("requested status is mutuals only but accounts aren't mufos")
|
l.Trace("requested status is mutuals only but accounts aren't mufos")
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
case gtsmodel.VisibilityDirect:
|
case gtsmodel.VisibilityDirect:
|
||||||
l.Debug("requesting account requests a status it's not mentioned in")
|
l.Trace("requesting account requests a status it's not mentioned in")
|
||||||
return false, nil // it's not mentioned -_-
|
return false, nil // it's not mentioned -_-
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -964,6 +1022,16 @@ func (ps *postgresService) PullRelevantAccountsFromStatus(targetStatus *gtsmodel
|
||||||
MentionedAccounts: []*gtsmodel.Account{},
|
MentionedAccounts: []*gtsmodel.Account{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get the author account
|
||||||
|
if targetStatus.GTSAuthorAccount == nil {
|
||||||
|
statusAuthor := >smodel.Account{}
|
||||||
|
if err := ps.conn.Model(statusAuthor).Where("id = ?", targetStatus.AccountID).Select(); err != nil {
|
||||||
|
return accounts, fmt.Errorf("PullRelevantAccountsFromStatus: error getting statusAuthor with id %s: %s", targetStatus.AccountID, err)
|
||||||
|
}
|
||||||
|
targetStatus.GTSAuthorAccount = statusAuthor
|
||||||
|
}
|
||||||
|
accounts.StatusAuthor = targetStatus.GTSAuthorAccount
|
||||||
|
|
||||||
// get the replied to account from the status and add it to the pile
|
// get the replied to account from the status and add it to the pile
|
||||||
if targetStatus.InReplyToAccountID != "" {
|
if targetStatus.InReplyToAccountID != "" {
|
||||||
repliedToAccount := >smodel.Account{}
|
repliedToAccount := >smodel.Account{}
|
||||||
|
@ -1042,55 +1110,6 @@ func (ps *postgresService) StatusBookmarkedBy(status *gtsmodel.Status, accountID
|
||||||
return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
return ps.conn.Model(>smodel.StatusBookmark{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (ps *postgresService) FaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
|
|
||||||
// // first check if a fave already exists, we can just return if so
|
|
||||||
// existingFave := >smodel.StatusFave{}
|
|
||||||
// err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
|
|
||||||
// if err == nil {
|
|
||||||
// // fave already exists so just return nothing at all
|
|
||||||
// return nil, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // an error occurred so it might exist or not, we don't know
|
|
||||||
// if err != pg.ErrNoRows {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // it doesn't exist so create it
|
|
||||||
// newFave := >smodel.StatusFave{
|
|
||||||
// AccountID: accountID,
|
|
||||||
// TargetAccountID: status.AccountID,
|
|
||||||
// StatusID: status.ID,
|
|
||||||
// }
|
|
||||||
// if _, err = ps.conn.Model(newFave).Insert(); err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return newFave, nil
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (ps *postgresService) UnfaveStatus(status *gtsmodel.Status, accountID string) (*gtsmodel.StatusFave, error) {
|
|
||||||
// if a fave doesn't exist, we don't need to do anything
|
|
||||||
existingFave := >smodel.StatusFave{}
|
|
||||||
err := ps.conn.Model(existingFave).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Select()
|
|
||||||
// the fave doesn't exist so return nothing at all
|
|
||||||
if err == pg.ErrNoRows {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// an error occurred so it might exist or not, we don't know
|
|
||||||
if err != nil && err != pg.ErrNoRows {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// the fave exists so remove it
|
|
||||||
if _, err = ps.conn.Model(>smodel.StatusFave{}).Where("status_id = ?", status.ID).Where("account_id = ?", accountID).Delete(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return existingFave, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) {
|
func (ps *postgresService) WhoFavedStatus(status *gtsmodel.Status) ([]*gtsmodel.Account, error) {
|
||||||
accounts := []*gtsmodel.Account{}
|
accounts := []*gtsmodel.Account{}
|
||||||
|
|
||||||
|
@ -1139,7 +1158,7 @@ func (ps *postgresService) WhoBoostedStatus(status *gtsmodel.Status) ([]*gtsmode
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
|
func (ps *postgresService) GetStatusesWhereFollowing(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*gtsmodel.Status, error) {
|
||||||
statuses := []*gtsmodel.Status{}
|
statuses := []*gtsmodel.Status{}
|
||||||
|
|
||||||
q := ps.conn.Model(&statuses)
|
q := ps.conn.Model(&statuses)
|
||||||
|
@ -1147,38 +1166,38 @@ func (ps *postgresService) GetHomeTimelineForAccount(accountID string, maxID str
|
||||||
q = q.ColumnExpr("status.*").
|
q = q.ColumnExpr("status.*").
|
||||||
Join("JOIN follows AS f ON f.target_account_id = status.account_id").
|
Join("JOIN follows AS f ON f.target_account_id = status.account_id").
|
||||||
Where("f.account_id = ?", accountID).
|
Where("f.account_id = ?", accountID).
|
||||||
Limit(limit).
|
Order("status.id DESC")
|
||||||
Order("status.created_at DESC")
|
|
||||||
|
|
||||||
if maxID != "" {
|
if maxID != "" {
|
||||||
s := >smodel.Status{}
|
q = q.Where("status.id < ?", maxID)
|
||||||
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("status.created_at < ?", s.CreatedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minID != "" {
|
|
||||||
s := >smodel.Status{}
|
|
||||||
if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("status.created_at > ?", s.CreatedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sinceID != "" {
|
if sinceID != "" {
|
||||||
s := >smodel.Status{}
|
q = q.Where("status.id > ?", sinceID)
|
||||||
if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
if minID != "" {
|
||||||
q = q.Where("status.created_at > ?", s.CreatedAt)
|
q = q.Where("status.id > ?", minID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if local {
|
||||||
|
q = q.Where("status.local = ?", local)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
q = q.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := q.Select()
|
err := q.Select()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
return nil, err
|
return nil, db.ErrNoEntries{}
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(statuses) == 0 {
|
||||||
|
return nil, db.ErrNoEntries{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
|
@ -1189,42 +1208,36 @@ func (ps *postgresService) GetPublicTimelineForAccount(accountID string, maxID s
|
||||||
|
|
||||||
q := ps.conn.Model(&statuses).
|
q := ps.conn.Model(&statuses).
|
||||||
Where("visibility = ?", gtsmodel.VisibilityPublic).
|
Where("visibility = ?", gtsmodel.VisibilityPublic).
|
||||||
Limit(limit).
|
Where("? IS NULL", pg.Ident("in_reply_to_id")).
|
||||||
Order("created_at DESC")
|
Where("? IS NULL", pg.Ident("boost_of_id")).
|
||||||
|
Order("status.id DESC")
|
||||||
|
|
||||||
if maxID != "" {
|
if maxID != "" {
|
||||||
s := >smodel.Status{}
|
q = q.Where("status.id < ?", maxID)
|
||||||
if err := ps.conn.Model(s).Where("id = ?", maxID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("created_at < ?", s.CreatedAt)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minID != "" {
|
|
||||||
s := >smodel.Status{}
|
|
||||||
if err := ps.conn.Model(s).Where("id = ?", minID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("created_at > ?", s.CreatedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sinceID != "" {
|
if sinceID != "" {
|
||||||
s := >smodel.Status{}
|
q = q.Where("status.id > ?", sinceID)
|
||||||
if err := ps.conn.Model(s).Where("id = ?", sinceID).Select(); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
if minID != "" {
|
||||||
q = q.Where("created_at > ?", s.CreatedAt)
|
q = q.Where("status.id > ?", minID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if local {
|
if local {
|
||||||
q = q.Where("local = ?", local)
|
q = q.Where("status.local = ?", local)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limit > 0 {
|
||||||
|
q = q.Limit(limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := q.Select()
|
err := q.Select()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
return nil, err
|
return nil, db.ErrNoEntries{}
|
||||||
}
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return statuses, nil
|
return statuses, nil
|
||||||
|
@ -1236,19 +1249,11 @@ func (ps *postgresService) GetNotificationsForAccount(accountID string, limit in
|
||||||
q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID)
|
q := ps.conn.Model(¬ifications).Where("target_account_id = ?", accountID)
|
||||||
|
|
||||||
if maxID != "" {
|
if maxID != "" {
|
||||||
n := >smodel.Notification{}
|
q = q.Where("id < ?", maxID)
|
||||||
if err := ps.conn.Model(n).Where("id = ?", maxID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("created_at < ?", n.CreatedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if sinceID != "" {
|
if sinceID != "" {
|
||||||
n := >smodel.Notification{}
|
q = q.Where("id > ?", sinceID)
|
||||||
if err := ps.conn.Model(n).Where("id = ?", sinceID).Select(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
q = q.Where("created_at > ?", n.CreatedAt)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if limit != 0 {
|
if limit != 0 {
|
||||||
|
@ -1270,6 +1275,8 @@ func (ps *postgresService) GetNotificationsForAccount(accountID string, limit in
|
||||||
CONVERSION FUNCTIONS
|
CONVERSION FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: move these to the type converter, it's bananas that they're here and not there
|
||||||
|
|
||||||
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) {
|
||||||
ogAccount := >smodel.Account{}
|
ogAccount := >smodel.Account{}
|
||||||
if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
|
if err := ps.conn.Model(ogAccount).Where("id = ?", originAccountID).Select(); err != nil {
|
||||||
|
@ -1313,12 +1320,14 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
|
||||||
// okay we're good now, we can start pulling accounts out of the database
|
// okay we're good now, we can start pulling accounts out of the database
|
||||||
mentionedAccount := >smodel.Account{}
|
mentionedAccount := >smodel.Account{}
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// match username + account, case insensitive
|
||||||
if local {
|
if local {
|
||||||
// local user -- should have a null domain
|
// local user -- should have a null domain
|
||||||
err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select()
|
err = ps.conn.Model(mentionedAccount).Where("LOWER(?) = LOWER(?)", pg.Ident("username"), username).Where("? IS NULL", pg.Ident("domain")).Select()
|
||||||
} else {
|
} else {
|
||||||
// remote user -- should have domain defined
|
// remote user -- should have domain defined
|
||||||
err = ps.conn.Model(mentionedAccount).Where("username = ?", username).Where("? = ?", pg.Ident("domain"), domain).Select()
|
err = ps.conn.Model(mentionedAccount).Where("LOWER(?) = LOWER(?)", pg.Ident("username"), username).Where("LOWER(?) = LOWER(?)", pg.Ident("domain"), domain).Select()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1339,6 +1348,7 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori
|
||||||
TargetAccountID: mentionedAccount.ID,
|
TargetAccountID: mentionedAccount.ID,
|
||||||
NameString: a,
|
NameString: a,
|
||||||
MentionedAccountURI: mentionedAccount.URI,
|
MentionedAccountURI: mentionedAccount.URI,
|
||||||
|
MentionedAccountURL: mentionedAccount.URL,
|
||||||
GTSAccount: mentionedAccount,
|
GTSAccount: mentionedAccount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1351,10 +1361,15 @@ func (ps *postgresService) TagStringsToTags(tags []string, originAccountID strin
|
||||||
tag := >smodel.Tag{}
|
tag := >smodel.Tag{}
|
||||||
// we can use selectorinsert here to create the new tag if it doesn't exist already
|
// we can use selectorinsert here to create the new tag if it doesn't exist already
|
||||||
// inserted will be true if this is a new tag we just created
|
// inserted will be true if this is a new tag we just created
|
||||||
if err := ps.conn.Model(tag).Where("name = ?", t).Select(); err != nil {
|
if err := ps.conn.Model(tag).Where("LOWER(?) = LOWER(?)", pg.Ident("name"), t).Select(); err != nil {
|
||||||
if err == pg.ErrNoRows {
|
if err == pg.ErrNoRows {
|
||||||
// tag doesn't exist yet so populate it
|
// tag doesn't exist yet so populate it
|
||||||
tag.ID = uuid.NewString()
|
newID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tag.ID = newID
|
||||||
|
tag.URL = fmt.Sprintf("%s://%s/tags/%s", ps.config.Protocol, ps.config.Host, t)
|
||||||
tag.Name = t
|
tag.Name = t
|
||||||
tag.FirstSeenFromAccountID = originAccountID
|
tag.FirstSeenFromAccountID = originAccountID
|
||||||
tag.CreatedAt = time.Now()
|
tag.CreatedAt = time.Now()
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -58,7 +59,43 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
|
||||||
}
|
}
|
||||||
|
|
||||||
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
|
for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
|
||||||
|
// check if the object is an IRI
|
||||||
|
if iter.IsIRI() {
|
||||||
|
// we have just the URI of whatever is being accepted, so we need to find out what it is
|
||||||
|
acceptedObjectIRI := iter.GetIRI()
|
||||||
|
if util.IsFollowPath(acceptedObjectIRI) {
|
||||||
|
// ACCEPT FOLLOW
|
||||||
|
gtsFollowRequest := >smodel.FollowRequest{}
|
||||||
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: acceptedObjectIRI.String()}}, gtsFollowRequest); err != nil {
|
||||||
|
return fmt.Errorf("ACCEPT: couldn't get follow request with id %s from the database: %s", acceptedObjectIRI.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the addressee of the original follow is the same as whatever inbox this landed in
|
||||||
|
if gtsFollowRequest.AccountID != inboxAcct.ID {
|
||||||
|
return errors.New("ACCEPT: follow object account and inbox account were not the same")
|
||||||
|
}
|
||||||
|
follow, err := f.db.AcceptFollowRequest(gtsFollowRequest.AccountID, gtsFollowRequest.TargetAccountID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fromFederatorChan <- gtsmodel.FromFederator{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsFollow,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsAccept,
|
||||||
|
GTSModel: follow,
|
||||||
|
ReceivingAccount: inboxAcct,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if iter is an AP object / type
|
||||||
|
if iter.GetType() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
switch iter.GetType().GetTypeName() {
|
switch iter.GetType().GetTypeName() {
|
||||||
|
// we have the whole object so we can figure out what we're accepting
|
||||||
case string(gtsmodel.ActivityStreamsFollow):
|
case string(gtsmodel.ActivityStreamsFollow):
|
||||||
// ACCEPT FOLLOW
|
// ACCEPT FOLLOW
|
||||||
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -99,10 +100,21 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error converting note to status: %s", err)
|
return fmt.Errorf("error converting note to status: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// id the status based on the time it was created
|
||||||
|
statusID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status.ID = statusID
|
||||||
|
|
||||||
if err := f.db.Put(status); err != nil {
|
if err := f.db.Put(status); err != nil {
|
||||||
if _, ok := err.(db.ErrAlreadyExists); ok {
|
if _, ok := err.(db.ErrAlreadyExists); ok {
|
||||||
|
// the status already exists in the database, which means we've already handled everything else,
|
||||||
|
// so we can just return nil here and be done with it.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// an actual error has happened
|
||||||
return fmt.Errorf("database error inserting status: %s", err)
|
return fmt.Errorf("database error inserting status: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +137,12 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
return fmt.Errorf("could not convert Follow to follow request: %s", err)
|
return fmt.Errorf("could not convert Follow to follow request: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
followRequest.ID = newID
|
||||||
|
|
||||||
if err := f.db.Put(followRequest); err != nil {
|
if err := f.db.Put(followRequest); err != nil {
|
||||||
return fmt.Errorf("database error inserting follow request: %s", err)
|
return fmt.Errorf("database error inserting follow request: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -146,6 +164,12 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
|
||||||
return fmt.Errorf("could not convert Like to fave: %s", err)
|
return fmt.Errorf("could not convert Like to fave: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fave.ID = newID
|
||||||
|
|
||||||
if err := f.db.Put(fave); err != nil {
|
if err := f.db.Put(fave); err != nil {
|
||||||
return fmt.Errorf("database error inserting fave: %s", err)
|
return fmt.Errorf("database error inserting fave: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower
|
||||||
}
|
}
|
||||||
|
|
||||||
acctFollowers := []gtsmodel.Follow{}
|
acctFollowers := []gtsmodel.Follow{}
|
||||||
if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers); err != nil {
|
if err := f.db.GetFollowersByAccountID(acct.ID, &acctFollowers, false); err != nil {
|
||||||
return nil, fmt.Errorf("db error getting followers for account id %s: %s", acct.ID, err)
|
return nil, fmt.Errorf("db error getting followers for account id %s: %s", acct.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,9 @@ func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo)
|
||||||
}
|
}
|
||||||
|
|
||||||
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
|
for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
|
||||||
|
if iter.GetType() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
switch iter.GetType().GetTypeName() {
|
switch iter.GetType().GetTypeName() {
|
||||||
case string(gtsmodel.ActivityStreamsFollow):
|
case string(gtsmodel.ActivityStreamsFollow):
|
||||||
// UNDO FOLLOW
|
// UNDO FOLLOW
|
||||||
|
|
|
@ -27,10 +27,10 @@
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor voc
|
||||||
//
|
//
|
||||||
// The go-fed library will handle setting the 'id' property on the
|
// The go-fed library will handle setting the 'id' property on the
|
||||||
// activity or object provided with the value returned.
|
// activity or object provided with the value returned.
|
||||||
func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) {
|
func (f *federatingDB) NewID(c context.Context, t vocab.Type) (idURL *url.URL, err error) {
|
||||||
l := f.log.WithFields(
|
l := f.log.WithFields(
|
||||||
logrus.Fields{
|
logrus.Fields{
|
||||||
"func": "NewID",
|
"func": "NewID",
|
||||||
|
@ -99,7 +99,11 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
|
||||||
if iter.IsIRI() {
|
if iter.IsIRI() {
|
||||||
actorAccount := >smodel.Account{}
|
actorAccount := >smodel.Account{}
|
||||||
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
|
if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
|
||||||
return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host, uuid.NewString()))
|
newID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host, newID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,8 +162,12 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback default behavior: just return a random UUID after our protocol and host
|
// fallback default behavior: just return a random ULID after our protocol and host
|
||||||
return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString()))
|
newID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, newID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
|
// ActorForOutbox fetches the actor's IRI for the given outbox IRI.
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,6 +143,12 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
|
||||||
return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err)
|
return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", publicKeyOwnerURI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return ctx, false, err
|
||||||
|
}
|
||||||
|
a.ID = aID
|
||||||
|
|
||||||
if err := f.db.Put(a); err != nil {
|
if err := f.db.Put(a); err != nil {
|
||||||
l.Errorf("error inserting dereferenced remote account: %s", err)
|
l.Errorf("error inserting dereferenced remote account: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package processing
|
package gtserror
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -24,12 +24,12 @@
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrorWithCode wraps an internal error with an http code, and a 'safe' version of
|
// WithCode wraps an internal error with an http code, and a 'safe' version of
|
||||||
// the error that can be served to clients without revealing internal business logic.
|
// the error that can be served to clients without revealing internal business logic.
|
||||||
//
|
//
|
||||||
// A typical use of this error would be to first log the Original error, then return
|
// A typical use of this error would be to first log the Original error, then return
|
||||||
// the Safe error and the StatusCode to an API caller.
|
// the Safe error and the StatusCode to an API caller.
|
||||||
type ErrorWithCode interface {
|
type WithCode interface {
|
||||||
// Error returns the original internal error for debugging within the GoToSocial logs.
|
// Error returns the original internal error for debugging within the GoToSocial logs.
|
||||||
// This should *NEVER* be returned to a client as it may contain sensitive information.
|
// This should *NEVER* be returned to a client as it may contain sensitive information.
|
||||||
Error() string
|
Error() string
|
||||||
|
@ -40,31 +40,31 @@ type ErrorWithCode interface {
|
||||||
Code() int
|
Code() int
|
||||||
}
|
}
|
||||||
|
|
||||||
type errorWithCode struct {
|
type withCode struct {
|
||||||
original error
|
original error
|
||||||
safe error
|
safe error
|
||||||
code int
|
code int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorWithCode) Error() string {
|
func (e withCode) Error() string {
|
||||||
return e.original.Error()
|
return e.original.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorWithCode) Safe() string {
|
func (e withCode) Safe() string {
|
||||||
return e.safe.Error()
|
return e.safe.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e errorWithCode) Code() int {
|
func (e withCode) Code() int {
|
||||||
return e.code
|
return e.code
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
|
// NewErrorBadRequest returns an ErrorWithCode 400 with the given original error and optional help text.
|
||||||
func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode {
|
func NewErrorBadRequest(original error, helpText ...string) WithCode {
|
||||||
safe := "bad request"
|
safe := "bad request"
|
||||||
if helpText != nil {
|
if helpText != nil {
|
||||||
safe = safe + ": " + strings.Join(helpText, ": ")
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
}
|
}
|
||||||
return errorWithCode{
|
return withCode{
|
||||||
original: original,
|
original: original,
|
||||||
safe: errors.New(safe),
|
safe: errors.New(safe),
|
||||||
code: http.StatusBadRequest,
|
code: http.StatusBadRequest,
|
||||||
|
@ -72,12 +72,12 @@ func NewErrorBadRequest(original error, helpText ...string) ErrorWithCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
|
// NewErrorNotAuthorized returns an ErrorWithCode 401 with the given original error and optional help text.
|
||||||
func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
|
func NewErrorNotAuthorized(original error, helpText ...string) WithCode {
|
||||||
safe := "not authorized"
|
safe := "not authorized"
|
||||||
if helpText != nil {
|
if helpText != nil {
|
||||||
safe = safe + ": " + strings.Join(helpText, ": ")
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
}
|
}
|
||||||
return errorWithCode{
|
return withCode{
|
||||||
original: original,
|
original: original,
|
||||||
safe: errors.New(safe),
|
safe: errors.New(safe),
|
||||||
code: http.StatusUnauthorized,
|
code: http.StatusUnauthorized,
|
||||||
|
@ -85,12 +85,12 @@ func NewErrorNotAuthorized(original error, helpText ...string) ErrorWithCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
|
// NewErrorForbidden returns an ErrorWithCode 403 with the given original error and optional help text.
|
||||||
func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
|
func NewErrorForbidden(original error, helpText ...string) WithCode {
|
||||||
safe := "forbidden"
|
safe := "forbidden"
|
||||||
if helpText != nil {
|
if helpText != nil {
|
||||||
safe = safe + ": " + strings.Join(helpText, ": ")
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
}
|
}
|
||||||
return errorWithCode{
|
return withCode{
|
||||||
original: original,
|
original: original,
|
||||||
safe: errors.New(safe),
|
safe: errors.New(safe),
|
||||||
code: http.StatusForbidden,
|
code: http.StatusForbidden,
|
||||||
|
@ -98,12 +98,12 @@ func NewErrorForbidden(original error, helpText ...string) ErrorWithCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
|
// NewErrorNotFound returns an ErrorWithCode 404 with the given original error and optional help text.
|
||||||
func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
|
func NewErrorNotFound(original error, helpText ...string) WithCode {
|
||||||
safe := "404 not found"
|
safe := "404 not found"
|
||||||
if helpText != nil {
|
if helpText != nil {
|
||||||
safe = safe + ": " + strings.Join(helpText, ": ")
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
}
|
}
|
||||||
return errorWithCode{
|
return withCode{
|
||||||
original: original,
|
original: original,
|
||||||
safe: errors.New(safe),
|
safe: errors.New(safe),
|
||||||
code: http.StatusNotFound,
|
code: http.StatusNotFound,
|
||||||
|
@ -111,12 +111,12 @@ func NewErrorNotFound(original error, helpText ...string) ErrorWithCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
|
// NewErrorInternalError returns an ErrorWithCode 500 with the given original error and optional help text.
|
||||||
func NewErrorInternalError(original error, helpText ...string) ErrorWithCode {
|
func NewErrorInternalError(original error, helpText ...string) WithCode {
|
||||||
safe := "internal server error"
|
safe := "internal server error"
|
||||||
if helpText != nil {
|
if helpText != nil {
|
||||||
safe = safe + ": " + strings.Join(helpText, ": ")
|
safe = safe + ": " + strings.Join(helpText, ": ")
|
||||||
}
|
}
|
||||||
return errorWithCode{
|
return withCode{
|
||||||
original: original,
|
original: original,
|
||||||
safe: errors.New(safe),
|
safe: errors.New(safe),
|
||||||
code: http.StatusInternalServerError,
|
code: http.StatusInternalServerError,
|
|
@ -1,5 +0,0 @@
|
||||||
# gtsmodel
|
|
||||||
|
|
||||||
This package contains types used *internally* by GoToSocial and added/removed/selected from the database. As such, they contain sensitive fields which should **never** be serialized or reach the API level. Use the [mastotypes](../../pkg/mastotypes) package for that.
|
|
||||||
|
|
||||||
The annotation used on these structs is for handling them via the go-pg ORM. See [here](https://pg.uptrace.dev/models/).
|
|
|
@ -33,8 +33,8 @@ type Account struct {
|
||||||
BASIC INFO
|
BASIC INFO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// id of this account in the local database; the end-user will never need to know this, it's strictly internal
|
// id of this account in the local database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``
|
// Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org``
|
||||||
Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other
|
Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other
|
||||||
// Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username.
|
// Domain of the account, will be null if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username.
|
||||||
|
@ -45,11 +45,11 @@ type Account struct {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ID of the avatar as a media attachment
|
// ID of the avatar as a media attachment
|
||||||
AvatarMediaAttachmentID string
|
AvatarMediaAttachmentID string `pg:"type:CHAR(26)"`
|
||||||
// For a non-local account, where can the header be fetched?
|
// For a non-local account, where can the header be fetched?
|
||||||
AvatarRemoteURL string
|
AvatarRemoteURL string
|
||||||
// ID of the header as a media attachment
|
// ID of the header as a media attachment
|
||||||
HeaderMediaAttachmentID string
|
HeaderMediaAttachmentID string `pg:"type:CHAR(26)"`
|
||||||
// For a non-local account, where can the header be fetched?
|
// For a non-local account, where can the header be fetched?
|
||||||
HeaderRemoteURL string
|
HeaderRemoteURL string
|
||||||
// DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
|
// DisplayName for this account. Can be empty, then just the Username will be used for display purposes.
|
||||||
|
@ -61,7 +61,7 @@ type Account struct {
|
||||||
// Is this a memorial account, ie., has the user passed away?
|
// Is this a memorial account, ie., has the user passed away?
|
||||||
Memorial bool
|
Memorial bool
|
||||||
// This account has moved this account id in the database
|
// This account has moved this account id in the database
|
||||||
MovedToAccountID string
|
MovedToAccountID string `pg:"type:CHAR(26)"`
|
||||||
// When was this account created?
|
// When was this account created?
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// When was this account last updated?
|
// When was this account last updated?
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
// It is used to authorize tokens etc, and is associated with an oauth client id in the database.
|
// It is used to authorize tokens etc, and is associated with an oauth client id in the database.
|
||||||
type Application struct {
|
type Application struct {
|
||||||
// id of this application in the db
|
// id of this application in the db
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
// name of the application given when it was created (eg., 'tusky')
|
// name of the application given when it was created (eg., 'tusky')
|
||||||
Name string
|
Name string
|
||||||
// website for the application given when it was created (eg., 'https://tusky.app')
|
// website for the application given when it was created (eg., 'https://tusky.app')
|
||||||
|
@ -30,7 +30,7 @@ type Application struct {
|
||||||
// redirect uri requested by the application for oauth2 flow
|
// redirect uri requested by the application for oauth2 flow
|
||||||
RedirectURI string
|
RedirectURI string
|
||||||
// id of the associated oauth client entity in the db
|
// id of the associated oauth client entity in the db
|
||||||
ClientID string
|
ClientID string `pg:"type:CHAR(26)"`
|
||||||
// secret of the associated oauth client entity in the db
|
// secret of the associated oauth client entity in the db
|
||||||
ClientSecret string
|
ClientSecret string
|
||||||
// scopes requested when this app was created
|
// scopes requested when this app was created
|
||||||
|
|
|
@ -5,15 +5,15 @@
|
||||||
// Block refers to the blocking of one account by another.
|
// Block refers to the blocking of one account by another.
|
||||||
type Block struct {
|
type Block struct {
|
||||||
// id of this block in the database
|
// id of this block in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
// When was this block created
|
// When was this block created
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// When was this block updated
|
// When was this block updated
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Who created this block?
|
// Who created this block?
|
||||||
AccountID string `pg:",notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// Who is targeted by this block?
|
// Who is targeted by this block?
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// Activitypub URI for this block
|
// Activitypub URI for this block
|
||||||
URI string
|
URI string
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// DomainBlock represents a federation block against a particular domain, of varying severity.
|
// DomainBlock represents a federation block against a particular domain, of varying severity.
|
||||||
type DomainBlock struct {
|
type DomainBlock struct {
|
||||||
// ID of this block in the database
|
// ID of this block in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// Domain to block. If ANY PART of the candidate domain contains this string, it will be blocked.
|
// Domain to block. If ANY PART of the candidate domain contains this string, it will be blocked.
|
||||||
// For example: 'example.org' also blocks 'gts.example.org'. '.com' blocks *any* '.com' domains.
|
// For example: 'example.org' also blocks 'gts.example.org'. '.com' blocks *any* '.com' domains.
|
||||||
// TODO: implement wildcards here
|
// TODO: implement wildcards here
|
||||||
|
@ -33,7 +33,7 @@ type DomainBlock struct {
|
||||||
// When was this block updated
|
// When was this block updated
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Account ID of the creator of this block
|
// Account ID of the creator of this block
|
||||||
CreatedByAccountID string `pg:",notnull"`
|
CreatedByAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// TODO: define this
|
// TODO: define this
|
||||||
Severity int
|
Severity int
|
||||||
// Reject media from this domain?
|
// Reject media from this domain?
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from.
|
// EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from.
|
||||||
type EmailDomainBlock struct {
|
type EmailDomainBlock struct {
|
||||||
// ID of this block in the database
|
// ID of this block in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// Email domain to block. Eg. 'gmail.com' or 'hotmail.com'
|
// Email domain to block. Eg. 'gmail.com' or 'hotmail.com'
|
||||||
Domain string `pg:",notnull"`
|
Domain string `pg:",notnull"`
|
||||||
// When was this block created
|
// When was this block created
|
||||||
|
@ -31,5 +31,5 @@ type EmailDomainBlock struct {
|
||||||
// When was this block updated
|
// When was this block updated
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Account ID of the creator of this block
|
// Account ID of the creator of this block
|
||||||
CreatedByAccountID string `pg:",notnull"`
|
CreatedByAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens.
|
// Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens.
|
||||||
type Emoji struct {
|
type Emoji struct {
|
||||||
// database ID of this emoji
|
// database ID of this emoji
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
// String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_
|
// String shortcode for this emoji -- the part that's between colons. This should be lowercase a-z_
|
||||||
// eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
// eg., 'blob_hug' 'purple_heart' Must be unique with domain.
|
||||||
Shortcode string `pg:",notnull,unique:shortcodedomain"`
|
Shortcode string `pg:",notnull,unique:shortcodedomain"`
|
||||||
|
@ -73,5 +73,5 @@ type Emoji struct {
|
||||||
// Is this emoji visible in the admin emoji picker?
|
// Is this emoji visible in the admin emoji picker?
|
||||||
VisibleInPicker bool `pg:",notnull,default:true"`
|
VisibleInPicker bool `pg:",notnull,default:true"`
|
||||||
// In which emoji category is this emoji visible?
|
// In which emoji category is this emoji visible?
|
||||||
CategoryID string
|
CategoryID string `pg:"type:CHAR(26)"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,15 +23,15 @@
|
||||||
// Follow represents one account following another, and the metadata around that follow.
|
// Follow represents one account following another, and the metadata around that follow.
|
||||||
type Follow struct {
|
type Follow struct {
|
||||||
// id of this follow in the database
|
// id of this follow in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// When was this follow created?
|
// When was this follow created?
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// When was this follow last updated?
|
// When was this follow last updated?
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Who does this follow belong to?
|
// Who does this follow belong to?
|
||||||
AccountID string `pg:",unique:srctarget,notnull"`
|
AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"`
|
||||||
// Who does AccountID follow?
|
// Who does AccountID follow?
|
||||||
TargetAccountID string `pg:",unique:srctarget,notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"`
|
||||||
// Does this follow also want to see reblogs and not just posts?
|
// Does this follow also want to see reblogs and not just posts?
|
||||||
ShowReblogs bool `pg:"default:true"`
|
ShowReblogs bool `pg:"default:true"`
|
||||||
// What is the activitypub URI of this follow?
|
// What is the activitypub URI of this follow?
|
||||||
|
|
|
@ -23,15 +23,15 @@
|
||||||
// FollowRequest represents one account requesting to follow another, and the metadata around that request.
|
// FollowRequest represents one account requesting to follow another, and the metadata around that request.
|
||||||
type FollowRequest struct {
|
type FollowRequest struct {
|
||||||
// id of this follow request in the database
|
// id of this follow request in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// When was this follow request created?
|
// When was this follow request created?
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// When was this follow request last updated?
|
// When was this follow request last updated?
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Who does this follow request originate from?
|
// Who does this follow request originate from?
|
||||||
AccountID string `pg:",unique:srctarget,notnull"`
|
AccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"`
|
||||||
// Who is the target of this follow request?
|
// Who is the target of this follow request?
|
||||||
TargetAccountID string `pg:",unique:srctarget,notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),unique:srctarget,notnull"`
|
||||||
// Does this follow also want to see reblogs and not just posts?
|
// Does this follow also want to see reblogs and not just posts?
|
||||||
ShowReblogs bool `pg:"default:true"`
|
ShowReblogs bool `pg:"default:true"`
|
||||||
// What is the activitypub URI of this follow request?
|
// What is the activitypub URI of this follow request?
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
// Instance represents a federated instance, either local or remote.
|
// Instance represents a federated instance, either local or remote.
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
// ID of this instance in the database
|
// ID of this instance in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// Instance domain eg example.org
|
// Instance domain eg example.org
|
||||||
Domain string `pg:",notnull,unique"`
|
Domain string `pg:",notnull,unique"`
|
||||||
// Title of this instance as it would like to be displayed.
|
// Title of this instance as it would like to be displayed.
|
||||||
|
@ -19,7 +19,7 @@ type Instance struct {
|
||||||
// When was this instance suspended, if at all?
|
// When was this instance suspended, if at all?
|
||||||
SuspendedAt time.Time
|
SuspendedAt time.Time
|
||||||
// ID of any existing domain block for this instance in the database
|
// ID of any existing domain block for this instance in the database
|
||||||
DomainBlockID string
|
DomainBlockID string `pg:"type:CHAR(26)"`
|
||||||
// Short description of this instance
|
// Short description of this instance
|
||||||
ShortDescription string
|
ShortDescription string
|
||||||
// Longer description of this instance
|
// Longer description of this instance
|
||||||
|
@ -27,7 +27,7 @@ type Instance struct {
|
||||||
// Contact email address for this instance
|
// Contact email address for this instance
|
||||||
ContactEmail string
|
ContactEmail string
|
||||||
// Contact account ID in the database for this instance
|
// Contact account ID in the database for this instance
|
||||||
ContactAccountID string
|
ContactAccountID string `pg:"type:CHAR(26)"`
|
||||||
// Reputation score of this instance
|
// Reputation score of this instance
|
||||||
Reputation int64 `pg:",notnull,default:0"`
|
Reputation int64 `pg:",notnull,default:0"`
|
||||||
// Version of the software used on this instance
|
// Version of the software used on this instance
|
||||||
|
|
|
@ -26,9 +26,9 @@
|
||||||
// somewhere in storage and that can be retrieved and served by the router.
|
// somewhere in storage and that can be retrieved and served by the router.
|
||||||
type MediaAttachment struct {
|
type MediaAttachment struct {
|
||||||
// ID of the attachment in the database
|
// ID of the attachment in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// ID of the status to which this is attached
|
// ID of the status to which this is attached
|
||||||
StatusID string
|
StatusID string `pg:"type:CHAR(26)"`
|
||||||
// Where can the attachment be retrieved on *this* server
|
// Where can the attachment be retrieved on *this* server
|
||||||
URL string
|
URL string
|
||||||
// Where can the attachment be retrieved on a remote server (empty for local media)
|
// Where can the attachment be retrieved on a remote server (empty for local media)
|
||||||
|
@ -42,11 +42,11 @@ type MediaAttachment struct {
|
||||||
// Metadata about the file
|
// Metadata about the file
|
||||||
FileMeta FileMeta
|
FileMeta FileMeta
|
||||||
// To which account does this attachment belong
|
// To which account does this attachment belong
|
||||||
AccountID string `pg:",notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// Description of the attachment (for screenreaders)
|
// Description of the attachment (for screenreaders)
|
||||||
Description string
|
Description string
|
||||||
// To which scheduled status does this attachment belong
|
// To which scheduled status does this attachment belong
|
||||||
ScheduledStatusID string
|
ScheduledStatusID string `pg:"type:CHAR(26)"`
|
||||||
// What is the generated blurhash of this attachment
|
// What is the generated blurhash of this attachment
|
||||||
Blurhash string
|
Blurhash string
|
||||||
// What is the processing status of this attachment
|
// What is the processing status of this attachment
|
||||||
|
|
|
@ -23,19 +23,19 @@
|
||||||
// Mention refers to the 'tagging' or 'mention' of a user within a status.
|
// Mention refers to the 'tagging' or 'mention' of a user within a status.
|
||||||
type Mention struct {
|
type Mention struct {
|
||||||
// ID of this mention in the database
|
// ID of this mention in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// ID of the status this mention originates from
|
// ID of the status this mention originates from
|
||||||
StatusID string `pg:",notnull"`
|
StatusID string `pg:"type:CHAR(26),notnull"`
|
||||||
// When was this mention created?
|
// When was this mention created?
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// When was this mention last updated?
|
// When was this mention last updated?
|
||||||
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// What's the internal account ID of the originator of the mention?
|
// What's the internal account ID of the originator of the mention?
|
||||||
OriginAccountID string `pg:",notnull"`
|
OriginAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// What's the AP URI of the originator of the mention?
|
// What's the AP URI of the originator of the mention?
|
||||||
OriginAccountURI string `pg:",notnull"`
|
OriginAccountURI string `pg:",notnull"`
|
||||||
// What's the internal account ID of the mention target?
|
// What's the internal account ID of the mention target?
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// Prevent this mention from generating a notification?
|
// Prevent this mention from generating a notification?
|
||||||
Silent bool
|
Silent bool
|
||||||
|
|
||||||
|
@ -56,6 +56,10 @@ type Mention struct {
|
||||||
//
|
//
|
||||||
// This will not be put in the database, it's just for convenience.
|
// This will not be put in the database, it's just for convenience.
|
||||||
MentionedAccountURI string `pg:"-"`
|
MentionedAccountURI string `pg:"-"`
|
||||||
|
// MentionedAccountURL is the web url of the user mentioned.
|
||||||
|
//
|
||||||
|
// This will not be put in the database, it's just for convenience.
|
||||||
|
MentionedAccountURL string `pg:"-"`
|
||||||
// A pointer to the gtsmodel account of the mentioned account.
|
// A pointer to the gtsmodel account of the mentioned account.
|
||||||
GTSAccount *Account `pg:"-"`
|
GTSAccount *Account `pg:"-"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,17 +23,17 @@
|
||||||
// Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc.
|
// Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc.
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
// ID of this notification in the database
|
// ID of this notification in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
// Type of this notification
|
// Type of this notification
|
||||||
NotificationType NotificationType `pg:",notnull"`
|
NotificationType NotificationType `pg:",notnull"`
|
||||||
// Creation time of this notification
|
// Creation time of this notification
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// Which account does this notification target (ie., who will receive the notification?)
|
// Which account does this notification target (ie., who will receive the notification?)
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// Which account performed the action that created this notification?
|
// Which account performed the action that created this notification?
|
||||||
OriginAccountID string `pg:",notnull"`
|
OriginAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// If the notification pertains to a status, what is the database ID of that status?
|
// If the notification pertains to a status, what is the database ID of that status?
|
||||||
StatusID string
|
StatusID string `pg:"type:CHAR(26)"`
|
||||||
// Has this notification been read already?
|
// Has this notification been read already?
|
||||||
Read bool
|
Read bool
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
// Status represents a user-created 'post' or 'status' in the database, either remote or local
|
||||||
type Status struct {
|
type Status struct {
|
||||||
// id of the status in the database
|
// id of the status in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
// uri at which this status is reachable
|
// uri at which this status is reachable
|
||||||
URI string `pg:",unique"`
|
URI string `pg:",unique"`
|
||||||
// web url for viewing this status
|
// web url for viewing this status
|
||||||
|
@ -45,13 +45,13 @@ type Status struct {
|
||||||
// is this status from a local account?
|
// is this status from a local account?
|
||||||
Local bool
|
Local bool
|
||||||
// which account posted this status?
|
// which account posted this status?
|
||||||
AccountID string
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// id of the status this status is a reply to
|
// id of the status this status is a reply to
|
||||||
InReplyToID string
|
InReplyToID string `pg:"type:CHAR(26)"`
|
||||||
// id of the account that this status replies to
|
// id of the account that this status replies to
|
||||||
InReplyToAccountID string
|
InReplyToAccountID string `pg:"type:CHAR(26)"`
|
||||||
// id of the status this status is a boost of
|
// id of the status this status is a boost of
|
||||||
BoostOfID string
|
BoostOfID string `pg:"type:CHAR(26)"`
|
||||||
// cw string for this status
|
// cw string for this status
|
||||||
ContentWarning string
|
ContentWarning string
|
||||||
// visibility entry for this status
|
// visibility entry for this status
|
||||||
|
@ -61,7 +61,7 @@ type Status struct {
|
||||||
// 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?
|
// Which application was used to create this status?
|
||||||
CreatedWithApplicationID string
|
CreatedWithApplicationID string `pg:"type:CHAR(26)"`
|
||||||
// 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
|
||||||
|
@ -153,6 +153,7 @@ type VisibilityAdvanced struct {
|
||||||
|
|
||||||
// RelevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status.
|
// RelevantAccounts denotes accounts that are replied to, boosted by, or mentioned in a status.
|
||||||
type RelevantAccounts struct {
|
type RelevantAccounts struct {
|
||||||
|
StatusAuthor *Account
|
||||||
ReplyToAccount *Account
|
ReplyToAccount *Account
|
||||||
BoostedAccount *Account
|
BoostedAccount *Account
|
||||||
BoostedReplyToAccount *Account
|
BoostedReplyToAccount *Account
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
// StatusBookmark refers to one account having a 'bookmark' of the status of another account
|
// StatusBookmark refers to one account having a 'bookmark' of the status of another account
|
||||||
type StatusBookmark struct {
|
type StatusBookmark struct {
|
||||||
// id of this bookmark in the database
|
// id of this bookmark in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// when was this bookmark created
|
// when was this bookmark created
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// id of the account that created ('did') the bookmarking
|
// id of the account that created ('did') the bookmarking
|
||||||
AccountID string `pg:",notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// id the account owning the bookmarked status
|
// id the account owning the bookmarked status
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// database id of the status that has been bookmarked
|
// database id of the status that has been bookmarked
|
||||||
StatusID string `pg:",notnull"`
|
StatusID string `pg:"type:CHAR(26),notnull"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,15 +23,15 @@
|
||||||
// StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account
|
// StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account
|
||||||
type StatusFave struct {
|
type StatusFave struct {
|
||||||
// id of this fave in the database
|
// id of this fave in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// when was this fave created
|
// when was this fave created
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// id of the account that created ('did') the fave
|
// id of the account that created ('did') the fave
|
||||||
AccountID string `pg:",notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// id the account owning the faved status
|
// id the account owning the faved status
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// database id of the status that has been 'faved'
|
// database id of the status that has been 'faved'
|
||||||
StatusID string `pg:",notnull"`
|
StatusID string `pg:"type:CHAR(26),notnull"`
|
||||||
// ActivityPub URI of this fave
|
// ActivityPub URI of this fave
|
||||||
URI string `pg:",notnull"`
|
URI string `pg:",notnull"`
|
||||||
|
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
// StatusMute refers to one account having muted the status of another account or its own
|
// StatusMute refers to one account having muted the status of another account or its own
|
||||||
type StatusMute struct {
|
type StatusMute struct {
|
||||||
// id of this mute in the database
|
// id of this mute in the database
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// when was this mute created
|
// when was this mute created
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// id of the account that created ('did') the mute
|
// id of the account that created ('did') the mute
|
||||||
AccountID string `pg:",notnull"`
|
AccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// id the account owning the muted status (can be the same as accountID)
|
// id the account owning the muted status (can be the same as accountID)
|
||||||
TargetAccountID string `pg:",notnull"`
|
TargetAccountID string `pg:"type:CHAR(26),notnull"`
|
||||||
// database id of the status that has been muted
|
// database id of the status that has been muted
|
||||||
StatusID string `pg:",notnull"`
|
StatusID string `pg:"type:CHAR(26),notnull"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,13 +23,13 @@
|
||||||
// Tag represents a hashtag for gathering public statuses together
|
// Tag represents a hashtag for gathering public statuses together
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
// id of this tag in the database
|
// id of this tag in the database
|
||||||
ID string `pg:",unique,type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:",unique,type:CHAR(26),pk,notnull"`
|
||||||
// Href of this tag, eg https://example.org/tags/somehashtag
|
// Href of this tag, eg https://example.org/tags/somehashtag
|
||||||
URL string
|
URL string
|
||||||
// name of this tag -- the tag without the hash part
|
// name of this tag -- the tag without the hash part
|
||||||
Name string `pg:",unique,pk,notnull"`
|
Name string `pg:",unique,pk,notnull"`
|
||||||
// Which account ID is the first one we saw using this tag?
|
// Which account ID is the first one we saw using this tag?
|
||||||
FirstSeenFromAccountID string
|
FirstSeenFromAccountID string `pg:"type:CHAR(26)"`
|
||||||
// when was this tag created
|
// when was this tag created
|
||||||
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"`
|
||||||
// when was this tag last updated
|
// when was this tag last updated
|
||||||
|
|
|
@ -31,11 +31,11 @@ type User struct {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// id of this user in the local database; the end-user will never need to know this, it's strictly internal
|
// id of this user in the local database; the end-user will never need to know this, it's strictly internal
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"`
|
ID string `pg:"type:CHAR(26),pk,notnull,unique"`
|
||||||
// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported
|
// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported
|
||||||
Email string `pg:"default:null,unique"`
|
Email string `pg:"default:null,unique"`
|
||||||
// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet)
|
// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet)
|
||||||
AccountID string `pg:"default:'',notnull,unique"`
|
AccountID string `pg:"type:CHAR(26),unique"`
|
||||||
// The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables
|
// The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables
|
||||||
EncryptedPassword string `pg:",notnull"`
|
EncryptedPassword string `pg:",notnull"`
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ type User struct {
|
||||||
// How many times has this user signed in?
|
// How many times has this user signed in?
|
||||||
SignInCount int
|
SignInCount int
|
||||||
// id of the user who invited this user (who let this guy in?)
|
// id of the user who invited this user (who let this guy in?)
|
||||||
InviteID string
|
InviteID string `pg:"type:CHAR(26)"`
|
||||||
// What languages does this user want to see?
|
// What languages does this user want to see?
|
||||||
ChosenLanguages []string
|
ChosenLanguages []string
|
||||||
// What languages does this user not want to see?
|
// What languages does this user not want to see?
|
||||||
|
@ -68,7 +68,7 @@ type User struct {
|
||||||
// In what timezone/locale is this user located?
|
// In what timezone/locale is this user located?
|
||||||
Locale string
|
Locale string
|
||||||
// Which application id created this user? See gtsmodel.Application
|
// Which application id created this user? See gtsmodel.Application
|
||||||
CreatedByApplicationID string
|
CreatedByApplicationID string `pg:"type:CHAR(26)"`
|
||||||
// When did we last contact this user
|
// When did we last contact this user
|
||||||
LastEmailedAt time.Time `pg:"type:timestamp"`
|
LastEmailedAt time.Time `pg:"type:timestamp"`
|
||||||
|
|
||||||
|
|
51
internal/id/ulid.go
Normal file
51
internal/id/ulid.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package id
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid"
|
||||||
|
)
|
||||||
|
|
||||||
|
const randomRange = 631152381 // ~20 years in seconds
|
||||||
|
|
||||||
|
// NewULID returns a new ULID string using the current time, or an error if something goes wrong.
|
||||||
|
func NewULID() (string, error) {
|
||||||
|
newUlid, err := ulid.New(ulid.Timestamp(time.Now()), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newUlid.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewULIDFromTime returns a new ULID string using the given time, or an error if something goes wrong.
|
||||||
|
func NewULIDFromTime(t time.Time) (string, error) {
|
||||||
|
newUlid, err := ulid.New(ulid.Timestamp(t), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newUlid.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRandomULID returns a new ULID string using a random time in an ~80 year range around the current datetime, or an error if something goes wrong.
|
||||||
|
func NewRandomULID() (string, error) {
|
||||||
|
b1, err := rand.Int(rand.Reader, big.NewInt(randomRange))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
r1 := time.Duration(int(b1.Int64()))
|
||||||
|
|
||||||
|
b2, err := rand.Int(rand.Reader, big.NewInt(randomRange))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
r2 := -time.Duration(int(b2.Int64()))
|
||||||
|
|
||||||
|
arbitraryTime := time.Now().Add(r1 * time.Second).Add(r2 * time.Second)
|
||||||
|
newUlid, err := ulid.New(ulid.Timestamp(arbitraryTime), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newUlid.String(), nil
|
||||||
|
}
|
|
@ -26,12 +26,12 @@
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/blob"
|
"github.com/superseriousbusiness/gotosocial/internal/blob"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -242,9 +242,11 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) (
|
||||||
// create the urls and storage paths
|
// create the urls and storage paths
|
||||||
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
||||||
|
|
||||||
// generate a uuid for the new emoji -- normally we could let the database do this for us,
|
// generate a id for the new emoji
|
||||||
// but we need it below so we should create it here instead.
|
newEmojiID, err := id.NewRandomULID()
|
||||||
newEmojiID := uuid.NewString()
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// webfinger uri for the emoji -- unrelated to actually serving the image
|
// webfinger uri for the emoji -- unrelated to actually serving the image
|
||||||
// will be something like https://example.org/emoji/70a7f3d7-7e35-4098-8ce3-9b5e8203bb9c
|
// will be something like https://example.org/emoji/70a7f3d7-7e35-4098-8ce3-9b5e8203bb9c
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string, mediaType Type, accountID string, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
||||||
|
@ -72,9 +72,12 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string
|
||||||
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it
|
// now put it in storage, take a new id for the name of the file so we don't store any unnecessary info about it
|
||||||
extension := strings.Split(contentType, "/")[1]
|
extension := strings.Split(contentType, "/")[1]
|
||||||
newMediaID := uuid.NewString()
|
newMediaID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
||||||
originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension)
|
originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension)
|
||||||
|
|
|
@ -24,8 +24,8 @@
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, contentType string, remoteURL string) (*gtsmodel.MediaAttachment, error) {
|
||||||
|
@ -58,9 +58,12 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co
|
||||||
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
return nil, fmt.Errorf("error deriving thumbnail: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// now put it in storage, take a new uuid for the name of the file so we don't store any unnecessary info about it
|
// now put it in storage, take a new id for the name of the file so we don't store any unnecessary info about it
|
||||||
extension := strings.Split(contentType, "/")[1]
|
extension := strings.Split(contentType, "/")[1]
|
||||||
newMediaID := uuid.NewString()
|
newMediaID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
URLbase := fmt.Sprintf("%s://%s%s", mh.config.StorageConfig.ServeProtocol, mh.config.StorageConfig.ServeHost, mh.config.StorageConfig.ServeBasePath)
|
||||||
originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
|
originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension)
|
||||||
|
|
|
@ -67,7 +67,7 @@ func (cs *clientStore) Delete(ctx context.Context, id string) error {
|
||||||
|
|
||||||
// Client is a handy little wrapper for typical oauth client details
|
// Client is a handy little wrapper for typical oauth client details
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
Secret string
|
Secret string
|
||||||
Domain string
|
Domain string
|
||||||
UserID string
|
UserID string
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/oauth2/v4"
|
"github.com/superseriousbusiness/oauth2/v4"
|
||||||
"github.com/superseriousbusiness/oauth2/v4/models"
|
"github.com/superseriousbusiness/oauth2/v4/models"
|
||||||
)
|
)
|
||||||
|
@ -98,7 +99,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error
|
||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("info param was not a models.Token")
|
return errors.New("info param was not a models.Token")
|
||||||
}
|
}
|
||||||
if err := pts.db.Put(TokenToPGToken(t)); err != nil {
|
|
||||||
|
pgt := TokenToPGToken(t)
|
||||||
|
if pgt.ID == "" {
|
||||||
|
pgtID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pgt.ID = pgtID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pts.db.Put(pgt); err != nil {
|
||||||
return fmt.Errorf("error in tokenstore create: %s", err)
|
return fmt.Errorf("error in tokenstore create: %s", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -176,7 +187,7 @@ func (pts *tokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2
|
||||||
// As such, manual translation is always required between Token and the gotosocial *model.Token. The helper functions oauthTokenToPGToken
|
// As such, manual translation is always required between Token and the gotosocial *model.Token. The helper functions oauthTokenToPGToken
|
||||||
// and pgTokenToOauthToken can be used for that.
|
// and pgTokenToOauthToken can be used for that.
|
||||||
type Token struct {
|
type Token struct {
|
||||||
ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"`
|
ID string `pg:"type:CHAR(26),pk,notnull"`
|
||||||
ClientID string
|
ClientID string
|
||||||
UserID string
|
UserID string
|
||||||
RedirectURI string
|
RedirectURI string
|
||||||
|
|
|
@ -22,10 +22,11 @@
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -202,13 +203,13 @@ func (p *processor) AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCrede
|
||||||
return acctSensitive, nil
|
return acctSensitive, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode) {
|
func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode) {
|
||||||
targetAccount := >smodel.Account{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
if err := p.db.GetByID(targetAccountID, targetAccount); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry found for account id %s", targetAccountID))
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses := []gtsmodel.Status{}
|
statuses := []gtsmodel.Status{}
|
||||||
|
@ -217,18 +218,18 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return apiStatuses, nil
|
return apiStatuses, nil
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s)
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(&s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant statuses: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.db.StatusVisible(&s, targetAccount, authed.Account, relevantAccounts)
|
visible, err := p.db.StatusVisible(&s, authed.Account, relevantAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking status visibility: %s", err))
|
||||||
}
|
}
|
||||||
if !visible {
|
if !visible {
|
||||||
continue
|
continue
|
||||||
|
@ -238,16 +239,16 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin
|
||||||
if s.BoostOfID != "" {
|
if s.BoostOfID != "" {
|
||||||
bs := >smodel.Status{}
|
bs := >smodel.Status{}
|
||||||
if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
|
if err := p.db.GetByID(s.BoostOfID, bs); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting boosted status: %s", err))
|
||||||
}
|
}
|
||||||
boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
|
boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relevant accounts from boosted status: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
|
boostedVisible, err := p.db.StatusVisible(bs, authed.Account, boostedRelevantAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking boosted status visibility: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if boostedVisible {
|
if boostedVisible {
|
||||||
|
@ -257,7 +258,7 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin
|
||||||
|
|
||||||
apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
|
apiStatus, err := p.tc.StatusToMasto(&s, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostedStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to masto: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatuses = append(apiStatuses, *apiStatus)
|
apiStatuses = append(apiStatuses, *apiStatus)
|
||||||
|
@ -266,29 +267,29 @@ func (p *processor) AccountStatusesGet(authed *oauth.Auth, targetAccountID strin
|
||||||
return apiStatuses, nil
|
return apiStatuses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
|
func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
followers := []gtsmodel.Follow{}
|
followers := []gtsmodel.Follow{}
|
||||||
accounts := []apimodel.Account{}
|
accounts := []apimodel.Account{}
|
||||||
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers); err != nil {
|
if err := p.db.GetFollowersByAccountID(targetAccountID, &followers, false); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range followers {
|
for _, f := range followers {
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
continue
|
continue
|
||||||
|
@ -299,7 +300,7 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// derefence account fields in case we haven't done it already
|
// derefence account fields in case we haven't done it already
|
||||||
|
@ -310,21 +311,21 @@ func (p *processor) AccountFollowersGet(authed *oauth.Auth, targetAccountID stri
|
||||||
|
|
||||||
account, err := p.tc.AccountToMastoPublic(a)
|
account, err := p.tc.AccountToMastoPublic(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
accounts = append(accounts, *account)
|
accounts = append(accounts, *account)
|
||||||
}
|
}
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode) {
|
func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
following := []gtsmodel.Follow{}
|
following := []gtsmodel.Follow{}
|
||||||
|
@ -333,13 +334,13 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, f := range following {
|
for _, f := range following {
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, f.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
continue
|
continue
|
||||||
|
@ -350,7 +351,7 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// derefence account fields in case we haven't done it already
|
// derefence account fields in case we haven't done it already
|
||||||
|
@ -361,53 +362,53 @@ func (p *processor) AccountFollowingGet(authed *oauth.Auth, targetAccountID stri
|
||||||
|
|
||||||
account, err := p.tc.AccountToMastoPublic(a)
|
account, err := p.tc.AccountToMastoPublic(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
accounts = append(accounts, *account)
|
accounts = append(accounts, *account)
|
||||||
}
|
}
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
func (p *processor) AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
if authed == nil || authed.Account == nil {
|
if authed == nil || authed.Account == nil {
|
||||||
return nil, NewErrorForbidden(errors.New("not authed"))
|
return nil, gtserror.NewErrorForbidden(errors.New("not authed"))
|
||||||
}
|
}
|
||||||
|
|
||||||
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
|
gtsR, err := p.db.GetRelationship(authed.Account.ID, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := p.tc.RelationshipToMasto(gtsR)
|
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode) {
|
func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// if there's a block between the accounts we shouldn't create the request ofc
|
// if there's a block between the accounts we shouldn't create the request ofc
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, form.TargetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: block exists between accounts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the target account actually exists in our db
|
// make sure the target account actually exists in our db
|
||||||
targetAcct := >smodel.Account{}
|
targetAcct := >smodel.Account{}
|
||||||
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
if err := p.db.GetByID(form.TargetAccountID, targetAcct); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.TargetAccountID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if a follow exists already
|
// check if a follow exists already
|
||||||
follows, err := p.db.Follows(authed.Account, targetAcct)
|
follows, err := p.db.Follows(authed.Account, targetAcct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err))
|
||||||
}
|
}
|
||||||
if follows {
|
if follows {
|
||||||
// already follows so just return the relationship
|
// already follows so just return the relationship
|
||||||
|
@ -417,7 +418,7 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
|
||||||
// check if a follow exists already
|
// check if a follow exists already
|
||||||
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
|
followRequested, err := p.db.FollowRequested(authed.Account, targetAcct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err))
|
||||||
}
|
}
|
||||||
if followRequested {
|
if followRequested {
|
||||||
// already follow requested so just return the relationship
|
// already follow requested so just return the relationship
|
||||||
|
@ -425,8 +426,10 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
|
||||||
}
|
}
|
||||||
|
|
||||||
// make the follow request
|
// make the follow request
|
||||||
|
newFollowID, err := id.NewRandomULID()
|
||||||
newFollowID := uuid.NewString()
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
fr := >smodel.FollowRequest{
|
fr := >smodel.FollowRequest{
|
||||||
ID: newFollowID,
|
ID: newFollowID,
|
||||||
|
@ -445,13 +448,13 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
|
||||||
|
|
||||||
// whack it in the database
|
// whack it in the database
|
||||||
if err := p.db.Put(fr); err != nil {
|
if err := p.db.Put(fr); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's a local account that's not locked we can just straight up accept the follow request
|
// if it's a local account that's not locked we can just straight up accept the follow request
|
||||||
if !targetAcct.Locked && targetAcct.Domain == "" {
|
if !targetAcct.Locked && targetAcct.Domain == "" {
|
||||||
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
|
if _, err := p.db.AcceptFollowRequest(authed.Account.ID, form.TargetAccountID); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err))
|
||||||
}
|
}
|
||||||
// return the new relationship
|
// return the new relationship
|
||||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
|
@ -470,21 +473,21 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou
|
||||||
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
return p.AccountRelationshipGet(authed, form.TargetAccountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode) {
|
func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
// if there's a block between the accounts we shouldn't do anything
|
// if there's a block between the accounts we shouldn't do anything
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, targetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the target account actually exists in our db
|
// make sure the target account actually exists in our db
|
||||||
targetAcct := >smodel.Account{}
|
targetAcct := >smodel.Account{}
|
||||||
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
|
if err := p.db.GetByID(targetAccountID, targetAcct); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,7 +501,7 @@ func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID stri
|
||||||
}, fr); err == nil {
|
}, fr); err == nil {
|
||||||
frURI = fr.URI
|
frURI = fr.URI
|
||||||
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
|
if err := p.db.DeleteByID(fr.ID, fr); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err))
|
||||||
}
|
}
|
||||||
frChanged = true
|
frChanged = true
|
||||||
}
|
}
|
||||||
|
@ -513,7 +516,7 @@ func (p *processor) AccountFollowRemove(authed *oauth.Auth, targetAccountID stri
|
||||||
}, f); err == nil {
|
}, f); err == nil {
|
||||||
fURI = f.URI
|
fURI = f.URI
|
||||||
if err := p.db.DeleteByID(f.ID, f); err != nil {
|
if err := p.db.DeleteByID(f.ID, f); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err))
|
||||||
}
|
}
|
||||||
fChanged = true
|
fChanged = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,6 +54,12 @@ func (p *processor) AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCre
|
||||||
return nil, fmt.Errorf("error reading emoji: %s", err)
|
return nil, fmt.Errorf("error reading emoji: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emojiID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
emoji.ID = emojiID
|
||||||
|
|
||||||
mastoEmoji, err := p.tc.EmojiToMasto(emoji)
|
mastoEmoji, err := p.tc.EmojiToMasto(emoji)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
|
return nil, fmt.Errorf("error converting emoji to mastotype: %s", err)
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,12 +36,21 @@ func (p *processor) AppCreate(authed *oauth.Auth, form *apimodel.ApplicationCrea
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate new IDs for this application and its associated client
|
// generate new IDs for this application and its associated client
|
||||||
clientID := uuid.NewString()
|
clientID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
clientSecret := uuid.NewString()
|
clientSecret := uuid.NewString()
|
||||||
vapidKey := uuid.NewString()
|
vapidKey := uuid.NewString()
|
||||||
|
|
||||||
|
appID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// generate the application to put in the database
|
// generate the application to put in the database
|
||||||
app := >smodel.Application{
|
app := >smodel.Application{
|
||||||
|
ID: appID,
|
||||||
Name: form.ClientName,
|
Name: form.ClientName,
|
||||||
Website: form.Website,
|
Website: form.Website,
|
||||||
RedirectURI: form.RedirectURIs,
|
RedirectURI: form.RedirectURIs,
|
||||||
|
|
|
@ -27,7 +27,9 @@
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -73,13 +75,18 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
|
||||||
return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
|
return nil, fmt.Errorf("couldn't convert dereferenced uri %s to gtsmodel account: %s", requestingAccountURI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shove it in the database for later
|
requestingAccountID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
requestingAccount.ID = requestingAccountID
|
||||||
|
|
||||||
if err := p.db.Put(requestingAccount); err != nil {
|
if err := p.db.Put(requestingAccount); err != nil {
|
||||||
return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
|
return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// put it in our channel to queue it for async processing
|
// put it in our channel to queue it for async processing
|
||||||
p.FromFederator() <- gtsmodel.FromFederator{
|
p.fromFederator <- gtsmodel.FromFederator{
|
||||||
APObjectType: gtsmodel.ActivityStreamsProfile,
|
APObjectType: gtsmodel.ActivityStreamsProfile,
|
||||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
GTSModel: requestingAccount,
|
GTSModel: requestingAccount,
|
||||||
|
@ -88,141 +95,141 @@ func (p *processor) authenticateAndDereferenceFediRequest(username string, r *ht
|
||||||
return requestingAccount, nil
|
return requestingAccount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
|
func (p *processor) GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate the request
|
// authenticate the request
|
||||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotAuthorized(err)
|
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedPerson, err := p.tc.AccountToAS(requestedAccount)
|
requestedPerson, err := p.tc.AccountToAS(requestedAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := streams.Serialize(requestedPerson)
|
data, err := streams.Serialize(requestedPerson)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
|
func (p *processor) GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate the request
|
// authenticate the request
|
||||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotAuthorized(err)
|
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
|
requestedFollowers, err := p.federator.FederatingDB().Followers(context.Background(), requestedAccountURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err))
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := streams.Serialize(requestedFollowers)
|
data, err := streams.Serialize(requestedFollowers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode) {
|
func (p *processor) GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate the request
|
// authenticate the request
|
||||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotAuthorized(err)
|
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
requestedAccountURI, err := url.Parse(requestedAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI)
|
requestedFollowing, err := p.federator.FederatingDB().Following(context.Background(), requestedAccountURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err))
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := streams.Serialize(requestedFollowing)
|
data, err := streams.Serialize(requestedFollowing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode) {
|
func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// authenticate the request
|
// authenticate the request
|
||||||
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
requestingAccount, err := p.authenticateAndDereferenceFediRequest(requestedUsername, request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotAuthorized(err)
|
return nil, gtserror.NewErrorNotAuthorized(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
blocked, err := p.db.Blocked(requestedAccount.ID, requestingAccount.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
return nil, gtserror.NewErrorNotAuthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
s := >smodel.Status{}
|
s := >smodel.Status{}
|
||||||
|
@ -230,27 +237,27 @@ func (p *processor) GetFediStatus(requestedUsername string, requestedStatusID st
|
||||||
{Key: "id", Value: requestedStatusID},
|
{Key: "id", Value: requestedStatusID},
|
||||||
{Key: "account_id", Value: requestedAccount.ID},
|
{Key: "account_id", Value: requestedAccount.ID},
|
||||||
}, s); err != nil {
|
}, s); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
asStatus, err := p.tc.StatusToAS(s)
|
asStatus, err := p.tc.StatusToAS(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := streams.Serialize(asStatus)
|
data, err := streams.Serialize(asStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) {
|
func (p *processor) GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode) {
|
||||||
// get the account the request is referring to
|
// get the account the request is referring to
|
||||||
requestedAccount := >smodel.Account{}
|
requestedAccount := >smodel.Account{}
|
||||||
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
if err := p.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the webfinger representation
|
// return the webfinger representation
|
||||||
|
|
|
@ -21,15 +21,16 @@
|
||||||
import (
|
import (
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode) {
|
func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) {
|
||||||
frs := []gtsmodel.FollowRequest{}
|
frs := []gtsmodel.FollowRequest{}
|
||||||
if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil {
|
if err := p.db.GetFollowRequestsForAccountID(auth.Account.ID, &frs); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,31 +38,31 @@ func (p *processor) FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, Err
|
||||||
for _, fr := range frs {
|
for _, fr := range frs {
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := p.db.GetByID(fr.AccountID, acct); err != nil {
|
if err := p.db.GetByID(fr.AccountID, acct); err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
mastoAcct, err := p.tc.AccountToMastoPublic(acct)
|
mastoAcct, err := p.tc.AccountToMastoPublic(acct)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
accts = append(accts, *mastoAcct)
|
accts = append(accts, *mastoAcct)
|
||||||
}
|
}
|
||||||
return accts, nil
|
return accts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode) {
|
func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) {
|
||||||
follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
|
follow, err := p.db.AcceptFollowRequest(accountID, auth.Account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
originAccount := >smodel.Account{}
|
originAccount := >smodel.Account{}
|
||||||
if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
|
if err := p.db.GetByID(follow.AccountID, originAccount); err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAccount := >smodel.Account{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
|
if err := p.db.GetByID(follow.TargetAccountID, targetAccount); err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
@ -74,17 +75,17 @@ func (p *processor) FollowRequestAccept(auth *oauth.Auth, accountID string) (*ap
|
||||||
|
|
||||||
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
gtsR, err := p.db.GetRelationship(auth.Account.ID, accountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := p.tc.RelationshipToMasto(gtsR)
|
r, err := p.tc.RelationshipToMasto(gtsR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) FollowRequestDeny(auth *oauth.Auth) ErrorWithCode {
|
func (p *processor) FollowRequestDeny(auth *oauth.Auth) gtserror.WithCode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,10 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
return errors.New("note was not parseable as *gtsmodel.Status")
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := p.timelineStatus(status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.notifyStatus(status); err != nil {
|
if err := p.notifyStatus(status); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -47,7 +51,6 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
if status.VisibilityAdvanced != nil && status.VisibilityAdvanced.Federated {
|
if status.VisibilityAdvanced != nil && status.VisibilityAdvanced.Federated {
|
||||||
return p.federateStatus(status)
|
return p.federateStatus(status)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
case gtsmodel.ActivityStreamsFollow:
|
case gtsmodel.ActivityStreamsFollow:
|
||||||
// CREATE FOLLOW REQUEST
|
// CREATE FOLLOW REQUEST
|
||||||
followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
|
followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest)
|
||||||
|
@ -124,6 +127,29 @@ func (p *processor) processFromClientAPI(clientMsg gtsmodel.FromClientAPI) error
|
||||||
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
return errors.New("undo was not parseable as *gtsmodel.Follow")
|
||||||
}
|
}
|
||||||
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
return p.federateUnfollow(follow, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
case gtsmodel.ActivityStreamsLike:
|
||||||
|
// UNDO LIKE/FAVE
|
||||||
|
fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("undo was not parseable as *gtsmodel.StatusFave")
|
||||||
|
}
|
||||||
|
return p.federateUnfave(fave, clientMsg.OriginAccount, clientMsg.TargetAccount)
|
||||||
|
}
|
||||||
|
case gtsmodel.ActivityStreamsDelete:
|
||||||
|
// DELETE
|
||||||
|
switch clientMsg.APObjectType {
|
||||||
|
case gtsmodel.ActivityStreamsNote:
|
||||||
|
// DELETE STATUS/NOTE
|
||||||
|
statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.deleteStatusFromTimelines(statusToDelete); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.federateStatusDelete(statusToDelete, clientMsg.OriginAccount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -144,6 +170,43 @@ func (p *processor) federateStatus(status *gtsmodel.Status) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateStatusDelete(status *gtsmodel.Status, originAccount *gtsmodel.Account) error {
|
||||||
|
asStatus, err := p.tc.StatusToAS(status)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateStatusDelete: error converting status to as format: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateStatusDelete: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actorIRI, err := url.Parse(originAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateStatusDelete: error parsing actorIRI %s: %s", originAccount.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a delete and set the appropriate actor on it
|
||||||
|
delete := streams.NewActivityStreamsDelete()
|
||||||
|
|
||||||
|
// set the actor for the delete
|
||||||
|
deleteActor := streams.NewActivityStreamsActorProperty()
|
||||||
|
deleteActor.AppendIRI(actorIRI)
|
||||||
|
delete.SetActivityStreamsActor(deleteActor)
|
||||||
|
|
||||||
|
// Set the status as the 'object' property.
|
||||||
|
deleteObject := streams.NewActivityStreamsObjectProperty()
|
||||||
|
deleteObject.AppendActivityStreamsNote(asStatus)
|
||||||
|
delete.SetActivityStreamsObject(deleteObject)
|
||||||
|
|
||||||
|
// set the to and cc as the original to/cc of the original status
|
||||||
|
delete.SetActivityStreamsTo(asStatus.GetActivityStreamsTo())
|
||||||
|
delete.SetActivityStreamsCc(asStatus.GetActivityStreamsCc())
|
||||||
|
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, delete)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *processor) federateFollow(followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// if both accounts are local there's nothing to do here
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
@ -207,6 +270,45 @@ func (p *processor) federateUnfollow(follow *gtsmodel.Follow, originAccount *gts
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) federateUnfave(fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
|
// if both accounts are local there's nothing to do here
|
||||||
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the AS fave
|
||||||
|
asFave, err := p.tc.FaveToAS(fave)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateFave: error converting fave to as format: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetAccountURI, err := url.Parse(targetAccount.URI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error parsing uri %s: %s", targetAccount.URI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an Undo and set the appropriate actor on it
|
||||||
|
undo := streams.NewActivityStreamsUndo()
|
||||||
|
undo.SetActivityStreamsActor(asFave.GetActivityStreamsActor())
|
||||||
|
|
||||||
|
// Set the fave as the 'object' property.
|
||||||
|
undoObject := streams.NewActivityStreamsObjectProperty()
|
||||||
|
undoObject.AppendActivityStreamsLike(asFave)
|
||||||
|
undo.SetActivityStreamsObject(undoObject)
|
||||||
|
|
||||||
|
// Set the To of the undo as the target of the fave
|
||||||
|
undoTo := streams.NewActivityStreamsToProperty()
|
||||||
|
undoTo.AppendIRI(targetAccountURI)
|
||||||
|
undo.SetActivityStreamsTo(undoTo)
|
||||||
|
|
||||||
|
outboxIRI, err := url.Parse(originAccount.OutboxURI)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("federateFave: error parsing outboxURI %s: %s", originAccount.OutboxURI, err)
|
||||||
|
}
|
||||||
|
_, err = p.federator.FederatingActor().Send(context.Background(), outboxIRI, undo)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
func (p *processor) federateAcceptFollowRequest(follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error {
|
||||||
// if both accounts are local there's nothing to do here
|
// if both accounts are local there's nothing to do here
|
||||||
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
if originAccount.Domain == "" && targetAccount.Domain == "" {
|
||||||
|
|
|
@ -20,9 +20,12 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
||||||
|
@ -77,7 +80,13 @@ func (p *processor) notifyStatus(status *gtsmodel.Status) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it
|
// if we've reached this point we know the mention is for a local account, and the notification doesn't exist, so create it
|
||||||
|
notifID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notif := >smodel.Notification{
|
notif := >smodel.Notification{
|
||||||
|
ID: notifID,
|
||||||
NotificationType: gtsmodel.NotificationMention,
|
NotificationType: gtsmodel.NotificationMention,
|
||||||
TargetAccountID: m.TargetAccountID,
|
TargetAccountID: m.TargetAccountID,
|
||||||
OriginAccountID: status.AccountID,
|
OriginAccountID: status.AccountID,
|
||||||
|
@ -98,7 +107,13 @@ func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, r
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notif := >smodel.Notification{
|
notif := >smodel.Notification{
|
||||||
|
ID: notifID,
|
||||||
NotificationType: gtsmodel.NotificationFollowRequest,
|
NotificationType: gtsmodel.NotificationFollowRequest,
|
||||||
TargetAccountID: followRequest.TargetAccountID,
|
TargetAccountID: followRequest.TargetAccountID,
|
||||||
OriginAccountID: followRequest.AccountID,
|
OriginAccountID: followRequest.AccountID,
|
||||||
|
@ -127,7 +142,13 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm
|
||||||
}
|
}
|
||||||
|
|
||||||
// now create the new follow notification
|
// now create the new follow notification
|
||||||
|
notifID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notif := >smodel.Notification{
|
notif := >smodel.Notification{
|
||||||
|
ID: notifID,
|
||||||
NotificationType: gtsmodel.NotificationFollow,
|
NotificationType: gtsmodel.NotificationFollow,
|
||||||
TargetAccountID: follow.TargetAccountID,
|
TargetAccountID: follow.TargetAccountID,
|
||||||
OriginAccountID: follow.AccountID,
|
OriginAccountID: follow.AccountID,
|
||||||
|
@ -145,7 +166,13 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notif := >smodel.Notification{
|
notif := >smodel.Notification{
|
||||||
|
ID: notifID,
|
||||||
NotificationType: gtsmodel.NotificationFave,
|
NotificationType: gtsmodel.NotificationFave,
|
||||||
TargetAccountID: fave.TargetAccountID,
|
TargetAccountID: fave.TargetAccountID,
|
||||||
OriginAccountID: fave.AccountID,
|
OriginAccountID: fave.AccountID,
|
||||||
|
@ -198,7 +225,13 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// now create the new reblog notification
|
// now create the new reblog notification
|
||||||
|
notifID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notif := >smodel.Notification{
|
notif := >smodel.Notification{
|
||||||
|
ID: notifID,
|
||||||
NotificationType: gtsmodel.NotificationReblog,
|
NotificationType: gtsmodel.NotificationReblog,
|
||||||
TargetAccountID: boostedAcct.ID,
|
TargetAccountID: boostedAcct.ID,
|
||||||
OriginAccountID: status.AccountID,
|
OriginAccountID: status.AccountID,
|
||||||
|
@ -211,3 +244,94 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) timelineStatus(status *gtsmodel.Status) error {
|
||||||
|
// make sure the author account is pinned onto the status
|
||||||
|
if status.GTSAuthorAccount == nil {
|
||||||
|
a := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(status.AccountID, a); err != nil {
|
||||||
|
return fmt.Errorf("timelineStatus: error getting author account with id %s: %s", status.AccountID, err)
|
||||||
|
}
|
||||||
|
status.GTSAuthorAccount = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all relevant accounts here once
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(status)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("timelineStatus: error getting relevant accounts from status: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get local followers of the account that posted the status
|
||||||
|
followers := []gtsmodel.Follow{}
|
||||||
|
if err := p.db.GetFollowersByAccountID(status.AccountID, &followers, true); err != nil {
|
||||||
|
return fmt.Errorf("timelineStatus: error getting followers for account id %s: %s", status.AccountID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the poster is local, add a fake entry for them to the followers list so they can see their own status in their timeline
|
||||||
|
if status.GTSAuthorAccount.Domain == "" {
|
||||||
|
followers = append(followers, gtsmodel.Follow{
|
||||||
|
AccountID: status.AccountID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(len(followers))
|
||||||
|
errors := make(chan error, len(followers))
|
||||||
|
|
||||||
|
for _, f := range followers {
|
||||||
|
go p.timelineStatusForAccount(status, f.AccountID, relevantAccounts, errors, &wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// read any errors that come in from the async functions
|
||||||
|
errs := []string{}
|
||||||
|
go func() {
|
||||||
|
for range errors {
|
||||||
|
e := <-errors
|
||||||
|
if e != nil {
|
||||||
|
errs = append(errs, e.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait til all functions have returned and then close the error channel
|
||||||
|
wg.Wait()
|
||||||
|
close(errors)
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
// we have some errors
|
||||||
|
return fmt.Errorf("timelineStatus: one or more errors timelining statuses: %s", strings.Join(errs, ";"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// no errors, nice
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) timelineStatusForAccount(status *gtsmodel.Status, accountID string, relevantAccounts *gtsmodel.RelevantAccounts, errors chan error, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
// get the targetAccount
|
||||||
|
timelineAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(accountID, timelineAccount); err != nil {
|
||||||
|
errors <- fmt.Errorf("timelineStatus: error getting account for timeline with id %s: %s", accountID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure the status is visible
|
||||||
|
visible, err := p.db.StatusVisible(status, timelineAccount, relevantAccounts)
|
||||||
|
if err != nil {
|
||||||
|
errors <- fmt.Errorf("timelineStatus: error getting visibility for status for timeline with id %s: %s", accountID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.timelineManager.IngestAndPrepare(status, timelineAccount.ID); err != nil {
|
||||||
|
errors <- fmt.Errorf("initTimelineFor: error ingesting status %s: %s", status.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) deleteStatusFromTimelines(status *gtsmodel.Status) error {
|
||||||
|
return p.timelineManager.WipeStatusFromAllTimelines(status.ID)
|
||||||
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error {
|
func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error {
|
||||||
|
@ -56,9 +56,14 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
return fmt.Errorf("error updating dereferenced status in the db: %s", err)
|
return fmt.Errorf("error updating dereferenced status in the db: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := p.timelineStatus(incomingStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.notifyStatus(incomingStatus); err != nil {
|
if err := p.notifyStatus(incomingStatus); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case gtsmodel.ActivityStreamsProfile:
|
case gtsmodel.ActivityStreamsProfile:
|
||||||
// CREATE AN ACCOUNT
|
// CREATE AN ACCOUNT
|
||||||
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
|
incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account)
|
||||||
|
@ -104,6 +109,12 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
return fmt.Errorf("error dereferencing announce from federator: %s", err)
|
return fmt.Errorf("error dereferencing announce from federator: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
incomingAnnounceID, err := id.NewULIDFromTime(incomingAnnounce.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
incomingAnnounce.ID = incomingAnnounceID
|
||||||
|
|
||||||
if err := p.db.Put(incomingAnnounce); err != nil {
|
if err := p.db.Put(incomingAnnounce); err != nil {
|
||||||
if _, ok := err.(db.ErrAlreadyExists); !ok {
|
if _, ok := err.(db.ErrAlreadyExists); !ok {
|
||||||
return fmt.Errorf("error adding dereferenced announce to the db: %s", err)
|
return fmt.Errorf("error adding dereferenced announce to the db: %s", err)
|
||||||
|
@ -141,6 +152,11 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er
|
||||||
// 1. delete all media associated with status
|
// 1. delete all media associated with status
|
||||||
// 2. delete boosts of status
|
// 2. delete boosts of status
|
||||||
// 3. etc etc etc
|
// 3. etc etc etc
|
||||||
|
statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("note was not parseable as *gtsmodel.Status")
|
||||||
|
}
|
||||||
|
return p.deleteStatusFromTimelines(statusToDelete)
|
||||||
case gtsmodel.ActivityStreamsProfile:
|
case gtsmodel.ActivityStreamsProfile:
|
||||||
// DELETE A PROFILE/ACCOUNT
|
// DELETE A PROFILE/ACCOUNT
|
||||||
// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
|
// TODO: handle side effects of account deletion here: delete all objects, statuses, media etc associated with account
|
||||||
|
@ -202,7 +218,11 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
|
||||||
// the status should have an ID by now, but just in case it doesn't let's generate one here
|
// the status should have an ID by now, but just in case it doesn't let's generate one here
|
||||||
// because we'll need it further down
|
// because we'll need it further down
|
||||||
if status.ID == "" {
|
if status.ID == "" {
|
||||||
status.ID = uuid.NewString()
|
newID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
status.ID = newID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Media attachments.
|
// 1. Media attachments.
|
||||||
|
@ -257,6 +277,14 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
|
||||||
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
|
// We should dereference any accounts mentioned here which we don't have in our db yet, by their URI.
|
||||||
mentions := []string{}
|
mentions := []string{}
|
||||||
for _, m := range status.GTSMentions {
|
for _, m := range status.GTSMentions {
|
||||||
|
if m.ID == "" {
|
||||||
|
mID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.ID = mID
|
||||||
|
}
|
||||||
|
|
||||||
uri, err := url.Parse(m.MentionedAccountURI)
|
uri, err := url.Parse(m.MentionedAccountURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
|
l.Debugf("error parsing mentioned account uri %s: %s", m.MentionedAccountURI, err)
|
||||||
|
@ -288,6 +316,12 @@ func (p *processor) dereferenceStatusFields(status *gtsmodel.Status, requestingU
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetAccountID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
targetAccount.ID = targetAccountID
|
||||||
|
|
||||||
if err := p.db.Put(targetAccount); err != nil {
|
if err := p.db.Put(targetAccount); err != nil {
|
||||||
return fmt.Errorf("db error inserting account with uri %s", uri.String())
|
return fmt.Errorf("db error inserting account with uri %s", uri.String())
|
||||||
}
|
}
|
||||||
|
@ -354,12 +388,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't have it so we need to dereference it
|
// we don't have it so we need to dereference it
|
||||||
remoteStatusID, err := url.Parse(announce.GTSBoostedStatus.URI)
|
remoteStatusURI, err := url.Parse(announce.GTSBoostedStatus.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
|
return fmt.Errorf("dereferenceAnnounce: error parsing url %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusID)
|
statusable, err := p.federator.DereferenceRemoteStatus(requestingUsername, remoteStatusURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err)
|
||||||
}
|
}
|
||||||
|
@ -387,7 +421,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
||||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
|
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced account with id %s into account : %s", accountURI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert the dereferenced account so it gets an ID etc
|
accountID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
account.ID = accountID
|
||||||
|
|
||||||
if err := p.db.Put(account); err != nil {
|
if err := p.db.Put(account); err != nil {
|
||||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
|
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err)
|
||||||
}
|
}
|
||||||
|
@ -403,7 +442,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse
|
||||||
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
|
return fmt.Errorf("dereferenceAnnounce: error converting dereferenced statusable with id %s into status : %s", announce.GTSBoostedStatus.URI, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// put it in the db already so it gets an ID generated for it
|
boostedStatusID, err := id.NewULIDFromTime(boostedStatus.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
boostedStatus.ID = boostedStatusID
|
||||||
|
|
||||||
if err := p.db.Put(boostedStatus); err != nil {
|
if err := p.db.Put(boostedStatus); err != nil {
|
||||||
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
|
return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,18 +23,19 @@
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode) {
|
func (p *processor) InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode) {
|
||||||
i := >smodel.Instance{}
|
i := >smodel.Instance{}
|
||||||
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
|
if err := p.db.GetWhere([]db.Where{{Key: "domain", Value: domain}}, i); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance %s: %s", p.config.Host, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
ai, err := p.tc.InstanceToMasto(i)
|
ai, err := p.tc.InstanceToMasto(i)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting instance to api representation: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return ai, nil
|
return ai, nil
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
@ -92,64 +93,64 @@ func (p *processor) MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentReq
|
||||||
return &mastoAttachment, nil
|
return &mastoAttachment, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, ErrorWithCode) {
|
func (p *processor) MediaGet(authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) {
|
||||||
attachment := >smodel.MediaAttachment{}
|
attachment := >smodel.MediaAttachment{}
|
||||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
// attachment doesn't exist
|
// attachment doesn't exist
|
||||||
return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||||
}
|
}
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if attachment.AccountID != authed.Account.ID {
|
if attachment.AccountID != authed.Account.ID {
|
||||||
return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := p.tc.AttachmentToMasto(attachment)
|
a, err := p.tc.AttachmentToMasto(attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &a, nil
|
return &a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode) {
|
func (p *processor) MediaUpdate(authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) {
|
||||||
attachment := >smodel.MediaAttachment{}
|
attachment := >smodel.MediaAttachment{}
|
||||||
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
if err := p.db.GetByID(mediaAttachmentID, attachment); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
// attachment doesn't exist
|
// attachment doesn't exist
|
||||||
return nil, NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
return nil, gtserror.NewErrorNotFound(errors.New("attachment doesn't exist in the db"))
|
||||||
}
|
}
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("db error getting attachment: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if attachment.AccountID != authed.Account.ID {
|
if attachment.AccountID != authed.Account.ID {
|
||||||
return nil, NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
return nil, gtserror.NewErrorNotFound(errors.New("attachment not owned by requesting account"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Description != nil {
|
if form.Description != nil {
|
||||||
attachment.Description = *form.Description
|
attachment.Description = *form.Description
|
||||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating description: %s", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Focus != nil {
|
if form.Focus != nil {
|
||||||
focusx, focusy, err := parseFocus(*form.Focus)
|
focusx, focusy, err := parseFocus(*form.Focus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorBadRequest(err)
|
return nil, gtserror.NewErrorBadRequest(err)
|
||||||
}
|
}
|
||||||
attachment.FileMeta.Focus.X = focusx
|
attachment.FileMeta.Focus.X = focusx
|
||||||
attachment.FileMeta.Focus.Y = focusy
|
attachment.FileMeta.Focus.Y = focusy
|
||||||
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
if err := p.db.UpdateByID(mediaAttachmentID, attachment); err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error updating focus: %s", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a, err := p.tc.AttachmentToMasto(attachment)
|
a, err := p.tc.AttachmentToMasto(attachment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error converting attachment: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &a, nil
|
return &a, nil
|
||||||
|
@ -159,37 +160,37 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest
|
||||||
// parse the form fields
|
// parse the form fields
|
||||||
mediaSize, err := media.ParseMediaSize(form.MediaSize)
|
mediaSize, err := media.ParseMediaSize(form.MediaSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaType, err := media.ParseMediaType(form.MediaType)
|
mediaType, err := media.ParseMediaType(form.MediaType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType))
|
||||||
}
|
}
|
||||||
|
|
||||||
spl := strings.Split(form.FileName, ".")
|
spl := strings.Split(form.FileName, ".")
|
||||||
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
|
if len(spl) != 2 || spl[0] == "" || spl[1] == "" {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("file name %s not parseable", form.FileName))
|
||||||
}
|
}
|
||||||
wantedMediaID := spl[0]
|
wantedMediaID := spl[0]
|
||||||
|
|
||||||
// get the account that owns the media and make sure it's not suspended
|
// get the account that owns the media and make sure it's not suspended
|
||||||
acct := >smodel.Account{}
|
acct := >smodel.Account{}
|
||||||
if err := p.db.GetByID(form.AccountID, acct); err != nil {
|
if err := p.db.GetByID(form.AccountID, acct); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s could not be selected from the db: %s", form.AccountID, err))
|
||||||
}
|
}
|
||||||
if !acct.SuspendedAt.IsZero() {
|
if !acct.SuspendedAt.IsZero() {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("account with id %s is suspended", form.AccountID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the requesting account and the media account don't block each other
|
// make sure the requesting account and the media account don't block each other
|
||||||
if authed.Account != nil {
|
if authed.Account != nil {
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
|
blocked, err := p.db.Blocked(authed.Account.ID, form.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err))
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,10 +202,10 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest
|
||||||
case media.Emoji:
|
case media.Emoji:
|
||||||
e := >smodel.Emoji{}
|
e := >smodel.Emoji{}
|
||||||
if err := p.db.GetByID(wantedMediaID, e); err != nil {
|
if err := p.db.GetByID(wantedMediaID, e); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s could not be taken from the db: %s", wantedMediaID, err))
|
||||||
}
|
}
|
||||||
if e.Disabled {
|
if e.Disabled {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji %s has been disabled", wantedMediaID))
|
||||||
}
|
}
|
||||||
switch mediaSize {
|
switch mediaSize {
|
||||||
case media.Original:
|
case media.Original:
|
||||||
|
@ -214,15 +215,15 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest
|
||||||
content.ContentType = e.ImageStaticContentType
|
content.ContentType = e.ImageStaticContentType
|
||||||
storagePath = e.ImageStaticPath
|
storagePath = e.ImageStaticPath
|
||||||
default:
|
default:
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize))
|
||||||
}
|
}
|
||||||
case media.Attachment, media.Header, media.Avatar:
|
case media.Attachment, media.Header, media.Avatar:
|
||||||
a := >smodel.MediaAttachment{}
|
a := >smodel.MediaAttachment{}
|
||||||
if err := p.db.GetByID(wantedMediaID, a); err != nil {
|
if err := p.db.GetByID(wantedMediaID, a); err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err))
|
||||||
}
|
}
|
||||||
if a.AccountID != form.AccountID {
|
if a.AccountID != form.AccountID {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("attachment %s is not owned by %s", wantedMediaID, form.AccountID))
|
||||||
}
|
}
|
||||||
switch mediaSize {
|
switch mediaSize {
|
||||||
case media.Original:
|
case media.Original:
|
||||||
|
@ -232,13 +233,13 @@ func (p *processor) FileGet(authed *oauth.Auth, form *apimodel.GetContentRequest
|
||||||
content.ContentType = a.Thumbnail.ContentType
|
content.ContentType = a.Thumbnail.ContentType
|
||||||
storagePath = a.Thumbnail.Path
|
storagePath = a.Thumbnail.Path
|
||||||
default:
|
default:
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not recognized for attachment", mediaSize))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := p.storage.RetrieveFileFrom(storagePath)
|
bytes, err := p.storage.RetrieveFileFrom(storagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error retrieving from storage: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
content.ContentLength = int64(len(bytes))
|
content.ContentLength = int64(len(bytes))
|
||||||
|
|
|
@ -20,15 +20,16 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, ErrorWithCode) {
|
func (p *processor) NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode) {
|
||||||
l := p.log.WithField("func", "NotificationsGet")
|
l := p.log.WithField("func", "NotificationsGet")
|
||||||
|
|
||||||
notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID, sinceID)
|
notifs, err := p.db.GetNotificationsForAccount(authed.Account.ID, limit, maxID, sinceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mastoNotifs := []*apimodel.Notification{}
|
mastoNotifs := []*apimodel.Notification{}
|
||||||
|
|
|
@ -28,9 +28,12 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/synchronous/status"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,14 +44,6 @@
|
||||||
// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
|
// fire messages into the processor and not wait for a reply before proceeding with other work. This allows
|
||||||
// for clean distribution of messages without slowing down the client API and harming the user experience.
|
// for clean distribution of messages without slowing down the client API and harming the user experience.
|
||||||
type Processor interface {
|
type Processor interface {
|
||||||
// ToClientAPI returns a channel for putting in messages that need to go to the gts client API.
|
|
||||||
// ToClientAPI() chan gtsmodel.ToClientAPI
|
|
||||||
// FromClientAPI returns a channel for putting messages in that come from the client api going to the processor
|
|
||||||
FromClientAPI() chan gtsmodel.FromClientAPI
|
|
||||||
// ToFederator returns a channel for putting in messages that need to go to the federator (activitypub).
|
|
||||||
// ToFederator() chan gtsmodel.ToFederator
|
|
||||||
// FromFederator returns a channel for putting messages in that come from the federator (activitypub) going into the processor
|
|
||||||
FromFederator() chan gtsmodel.FromFederator
|
|
||||||
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
||||||
Start() error
|
Start() error
|
||||||
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
|
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
|
||||||
|
@ -70,17 +65,17 @@ type Processor interface {
|
||||||
AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
|
AccountUpdate(authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, error)
|
||||||
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for
|
||||||
// the account given in authed.
|
// the account given in authed.
|
||||||
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, ErrorWithCode)
|
AccountStatusesGet(authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, maxID string, pinned bool, mediaOnly bool) ([]apimodel.Status, gtserror.WithCode)
|
||||||
// AccountFollowersGet fetches a list of the target account's followers.
|
// AccountFollowersGet fetches a list of the target account's followers.
|
||||||
AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
|
AccountFollowersGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||||
// AccountFollowingGet fetches a list of the accounts that target account is following.
|
// AccountFollowingGet fetches a list of the accounts that target account is following.
|
||||||
AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, ErrorWithCode)
|
AccountFollowingGet(authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode)
|
||||||
// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
// AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account.
|
||||||
AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
|
AccountRelationshipGet(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
// AccountFollowCreate handles a follow request to an account, either remote or local.
|
// AccountFollowCreate handles a follow request to an account, either remote or local.
|
||||||
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, ErrorWithCode)
|
AccountFollowCreate(authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
// AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local.
|
||||||
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, ErrorWithCode)
|
AccountFollowRemove(authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
|
||||||
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
// AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form.
|
||||||
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
AdminEmojiCreate(authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, error)
|
||||||
|
@ -92,25 +87,25 @@ type Processor interface {
|
||||||
FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
FileGet(authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, error)
|
||||||
|
|
||||||
// FollowRequestsGet handles the getting of the authed account's incoming follow requests
|
// FollowRequestsGet handles the getting of the authed account's incoming follow requests
|
||||||
FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, ErrorWithCode)
|
FollowRequestsGet(auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode)
|
||||||
// FollowRequestAccept handles the acceptance of a follow request from the given account ID
|
// FollowRequestAccept handles the acceptance of a follow request from the given account ID
|
||||||
FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, ErrorWithCode)
|
FollowRequestAccept(auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode)
|
||||||
|
|
||||||
// InstanceGet retrieves instance information for serving at api/v1/instance
|
// InstanceGet retrieves instance information for serving at api/v1/instance
|
||||||
InstanceGet(domain string) (*apimodel.Instance, ErrorWithCode)
|
InstanceGet(domain string) (*apimodel.Instance, gtserror.WithCode)
|
||||||
|
|
||||||
// MediaCreate handles the creation of a media attachment, using the given form.
|
// MediaCreate handles the creation of a media attachment, using the given form.
|
||||||
MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
|
MediaCreate(authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, error)
|
||||||
// MediaGet handles the GET of a media attachment with the given ID
|
// MediaGet handles the GET of a media attachment with the given ID
|
||||||
MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, ErrorWithCode)
|
MediaGet(authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode)
|
||||||
// MediaUpdate handles the PUT of a media attachment with the given ID and form
|
// MediaUpdate handles the PUT of a media attachment with the given ID and form
|
||||||
MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, ErrorWithCode)
|
MediaUpdate(authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode)
|
||||||
|
|
||||||
// NotificationsGet
|
// NotificationsGet
|
||||||
NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, ErrorWithCode)
|
NotificationsGet(authed *oauth.Auth, limit int, maxID string, sinceID string) ([]*apimodel.Notification, gtserror.WithCode)
|
||||||
|
|
||||||
// SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
|
// SearchGet performs a search with the given params, resolving/dereferencing remotely as desired
|
||||||
SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode)
|
SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode)
|
||||||
|
|
||||||
// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
|
// StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK.
|
||||||
StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
|
StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error)
|
||||||
|
@ -119,9 +114,9 @@ type Processor interface {
|
||||||
// StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
|
// StatusFave processes the faving of a given status, returning the updated status if the fave goes through.
|
||||||
StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
||||||
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
// StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
||||||
StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode)
|
StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, ErrorWithCode)
|
StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
||||||
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
// StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
|
StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error)
|
||||||
// StatusGet gets the given status, taking account of privacy settings and blocks etc.
|
// StatusGet gets the given status, taking account of privacy settings and blocks etc.
|
||||||
|
@ -129,12 +124,12 @@ type Processor interface {
|
||||||
// StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
|
// StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through.
|
||||||
StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error)
|
||||||
// StatusGetContext returns the context (previous and following posts) from the given status ID
|
// StatusGetContext returns the context (previous and following posts) from the given status ID
|
||||||
StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, ErrorWithCode)
|
StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
||||||
|
|
||||||
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
|
// HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters.
|
||||||
HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode)
|
HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode)
|
||||||
// PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters.
|
// PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters.
|
||||||
PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode)
|
PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FEDERATION API-FACING PROCESSING FUNCTIONS
|
FEDERATION API-FACING PROCESSING FUNCTIONS
|
||||||
|
@ -146,22 +141,22 @@ type Processor interface {
|
||||||
|
|
||||||
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
|
// GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication
|
||||||
// before returning a JSON serializable interface to the caller.
|
// before returning a JSON serializable interface to the caller.
|
||||||
GetFediUser(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
GetFediUser(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||||
|
|
||||||
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
|
// GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate
|
||||||
// authentication before returning a JSON serializable interface to the caller.
|
// authentication before returning a JSON serializable interface to the caller.
|
||||||
GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
GetFediFollowers(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||||
|
|
||||||
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
|
// GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate
|
||||||
// authentication before returning a JSON serializable interface to the caller.
|
// authentication before returning a JSON serializable interface to the caller.
|
||||||
GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, ErrorWithCode)
|
GetFediFollowing(requestedUsername string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||||
|
|
||||||
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
|
// GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate
|
||||||
// authentication before returning a JSON serializable interface to the caller.
|
// authentication before returning a JSON serializable interface to the caller.
|
||||||
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, ErrorWithCode)
|
GetFediStatus(requestedUsername string, requestedStatusID string, request *http.Request) (interface{}, gtserror.WithCode)
|
||||||
|
|
||||||
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
// GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups.
|
||||||
GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode)
|
GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, gtserror.WithCode)
|
||||||
|
|
||||||
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
|
// InboxPost handles POST requests to a user's inbox for new activitypub messages.
|
||||||
//
|
//
|
||||||
|
@ -178,57 +173,52 @@ type Processor interface {
|
||||||
|
|
||||||
// processor just implements the Processor interface
|
// processor just implements the Processor interface
|
||||||
type processor struct {
|
type processor struct {
|
||||||
// federator pub.FederatingActor
|
fromClientAPI chan gtsmodel.FromClientAPI
|
||||||
// toClientAPI chan gtsmodel.ToClientAPI
|
fromFederator chan gtsmodel.FromFederator
|
||||||
fromClientAPI chan gtsmodel.FromClientAPI
|
federator federation.Federator
|
||||||
// toFederator chan gtsmodel.ToFederator
|
stop chan interface{}
|
||||||
fromFederator chan gtsmodel.FromFederator
|
log *logrus.Logger
|
||||||
federator federation.Federator
|
config *config.Config
|
||||||
stop chan interface{}
|
tc typeutils.TypeConverter
|
||||||
log *logrus.Logger
|
oauthServer oauth.Server
|
||||||
config *config.Config
|
mediaHandler media.Handler
|
||||||
tc typeutils.TypeConverter
|
storage blob.Storage
|
||||||
oauthServer oauth.Server
|
timelineManager timeline.Manager
|
||||||
mediaHandler media.Handler
|
db db.DB
|
||||||
storage blob.Storage
|
|
||||||
db db.DB
|
/*
|
||||||
|
SUB-PROCESSORS
|
||||||
|
*/
|
||||||
|
|
||||||
|
statusProcessor status.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProcessor returns a new Processor that uses the given federator and logger
|
// NewProcessor returns a new Processor that uses the given federator and logger
|
||||||
func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage blob.Storage, db db.DB, log *logrus.Logger) Processor {
|
func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage blob.Storage, timelineManager timeline.Manager, db db.DB, log *logrus.Logger) Processor {
|
||||||
|
|
||||||
|
fromClientAPI := make(chan gtsmodel.FromClientAPI, 1000)
|
||||||
|
fromFederator := make(chan gtsmodel.FromFederator, 1000)
|
||||||
|
|
||||||
|
statusProcessor := status.New(db, tc, config, fromClientAPI, log)
|
||||||
|
|
||||||
return &processor{
|
return &processor{
|
||||||
// toClientAPI: make(chan gtsmodel.ToClientAPI, 100),
|
fromClientAPI: fromClientAPI,
|
||||||
fromClientAPI: make(chan gtsmodel.FromClientAPI, 100),
|
fromFederator: fromFederator,
|
||||||
// toFederator: make(chan gtsmodel.ToFederator, 100),
|
federator: federator,
|
||||||
fromFederator: make(chan gtsmodel.FromFederator, 100),
|
stop: make(chan interface{}),
|
||||||
federator: federator,
|
log: log,
|
||||||
stop: make(chan interface{}),
|
config: config,
|
||||||
log: log,
|
tc: tc,
|
||||||
config: config,
|
oauthServer: oauthServer,
|
||||||
tc: tc,
|
mediaHandler: mediaHandler,
|
||||||
oauthServer: oauthServer,
|
storage: storage,
|
||||||
mediaHandler: mediaHandler,
|
timelineManager: timelineManager,
|
||||||
storage: storage,
|
db: db,
|
||||||
db: db,
|
|
||||||
|
statusProcessor: statusProcessor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (p *processor) ToClientAPI() chan gtsmodel.ToClientAPI {
|
|
||||||
// return p.toClientAPI
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (p *processor) FromClientAPI() chan gtsmodel.FromClientAPI {
|
|
||||||
return p.fromClientAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// func (p *processor) ToFederator() chan gtsmodel.ToFederator {
|
|
||||||
// return p.toFederator
|
|
||||||
// }
|
|
||||||
|
|
||||||
func (p *processor) FromFederator() chan gtsmodel.FromFederator {
|
|
||||||
return p.fromFederator
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
// Start starts the Processor, reading from its channels and passing messages back and forth.
|
||||||
func (p *processor) Start() error {
|
func (p *processor) Start() error {
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -250,7 +240,7 @@ func (p *processor) Start() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
return nil
|
return p.initTimelines()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
|
// Stop stops the processor cleanly, finishing handling any remaining messages before closing down.
|
||||||
|
|
|
@ -27,12 +27,14 @@
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, ErrorWithCode) {
|
func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) {
|
||||||
l := p.log.WithFields(logrus.Fields{
|
l := p.log.WithFields(logrus.Fields{
|
||||||
"func": "SearchGet",
|
"func": "SearchGet",
|
||||||
"query": searchQuery.Query,
|
"query": searchQuery.Query,
|
||||||
|
@ -108,7 +110,7 @@ func (p *processor) SearchGet(authed *oauth.Auth, searchQuery *apimodel.SearchQu
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if visible, err := p.db.StatusVisible(foundStatus, statusOwner, authed.Account, relevantAccounts); !visible || err != nil {
|
if visible, err := p.db.StatusVisible(foundStatus, authed.Account, relevantAccounts); !visible || err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,10 +166,15 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve
|
||||||
// first turn it into a gtsmodel.Status
|
// first turn it into a gtsmodel.Status
|
||||||
status, err := p.tc.ASStatusToStatus(statusable)
|
status, err := p.tc.ASStatusToStatus(statusable)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// put it in the DB so it gets a UUID
|
statusID, err := id.NewULIDFromTime(status.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
status.ID = statusID
|
||||||
|
|
||||||
if err := p.db.Put(status); err != nil {
|
if err := p.db.Put(status); err != nil {
|
||||||
return nil, fmt.Errorf("error putting status in the db: %s", err)
|
return nil, fmt.Errorf("error putting status in the db: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -210,6 +217,12 @@ func (p *processor) searchAccountByURI(authed *oauth.Auth, uri *url.URL, resolve
|
||||||
return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
|
return nil, fmt.Errorf("searchAccountByURI: error dereferencing account with uri %s: %s", uri.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
accountID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
account.ID = accountID
|
||||||
|
|
||||||
if err := p.db.Put(account); err != nil {
|
if err := p.db.Put(account); err != nil {
|
||||||
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
|
return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err)
|
||||||
}
|
}
|
||||||
|
@ -280,6 +293,12 @@ func (p *processor) searchAccountByMention(authed *oauth.Auth, mention string, r
|
||||||
return nil, fmt.Errorf("searchAccountByMention: error converting account with uri %s: %s", acctURI.String(), err)
|
return nil, fmt.Errorf("searchAccountByMention: error converting account with uri %s: %s", acctURI.String(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foundAccountID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
foundAccount.ID = foundAccountID
|
||||||
|
|
||||||
// put this new account in our database
|
// put this new account in our database
|
||||||
if err := p.db.Put(foundAccount); err != nil {
|
if err := p.db.Put(foundAccount); err != nil {
|
||||||
return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err)
|
return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err)
|
||||||
|
|
|
@ -19,531 +19,43 @@
|
||||||
package processing
|
package processing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) StatusCreate(auth *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
|
func (p *processor) StatusCreate(authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, error) {
|
||||||
uris := util.GenerateURIsForAccount(auth.Account.Username, p.config.Protocol, p.config.Host)
|
return p.statusProcessor.Create(authed.Account, authed.Application, form)
|
||||||
thisStatusID := uuid.NewString()
|
|
||||||
thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
|
|
||||||
thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
|
|
||||||
newStatus := >smodel.Status{
|
|
||||||
ID: thisStatusID,
|
|
||||||
URI: thisStatusURI,
|
|
||||||
URL: thisStatusURL,
|
|
||||||
Content: util.HTMLFormat(form.Status),
|
|
||||||
CreatedAt: time.Now(),
|
|
||||||
UpdatedAt: time.Now(),
|
|
||||||
Local: true,
|
|
||||||
AccountID: auth.Account.ID,
|
|
||||||
ContentWarning: form.SpoilerText,
|
|
||||||
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
|
||||||
Sensitive: form.Sensitive,
|
|
||||||
Language: form.Language,
|
|
||||||
CreatedWithApplicationID: auth.Application.ID,
|
|
||||||
Text: form.Status,
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if replyToID is ok
|
|
||||||
if err := p.processReplyToID(form, auth.Account.ID, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if mediaIDs are ok
|
|
||||||
if err := p.processMediaIDs(form, auth.Account.ID, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if visibility settings are ok
|
|
||||||
if err := p.processVisibility(form, auth.Account.Privacy, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle language settings
|
|
||||||
if err := p.processLanguage(form, auth.Account.Language, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle mentions
|
|
||||||
if err := p.processMentions(form, auth.Account.ID, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.processTags(form, auth.Account.ID, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.processEmojis(form, auth.Account.ID, newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// put the new status in the database, generating an ID for it in the process
|
|
||||||
if err := p.db.Put(newStatus); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// change the status ID of the media attachments to the new status
|
|
||||||
for _, a := range newStatus.GTSMediaAttachments {
|
|
||||||
a.StatusID = newStatus.ID
|
|
||||||
a.UpdatedAt = time.Now()
|
|
||||||
if err := p.db.UpdateByID(a.ID, a); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// put the new status in the appropriate channel for async processing
|
|
||||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
|
||||||
APObjectType: newStatus.ActivityStreamsType,
|
|
||||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
|
||||||
GTSModel: newStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the frontend representation of the new status to the submitter
|
|
||||||
return p.tc.StatusToMasto(newStatus, auth.Account, auth.Account, nil, newStatus.GTSReplyToAccount, nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
func (p *processor) StatusDelete(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
||||||
l := p.log.WithField("func", "StatusDelete")
|
return p.statusProcessor.Delete(authed.Account, targetStatusID)
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetStatus.AccountID != authed.Account.ID {
|
|
||||||
return nil, errors.New("status doesn't belong to requesting account")
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var boostOfStatus *gtsmodel.Status
|
|
||||||
if targetStatus.BoostOfID != "" {
|
|
||||||
boostOfStatus = >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mastoStatus, err := p.tc.StatusToMasto(targetStatus, authed.Account, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.db.DeleteByID(targetStatus.ID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error deleting status from the database: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
func (p *processor) StatusFave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
||||||
l := p.log.WithField("func", "StatusFave")
|
return p.statusProcessor.Fave(authed.Account, targetStatusID)
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var boostOfStatus *gtsmodel.Status
|
|
||||||
if targetStatus.BoostOfID != "" {
|
|
||||||
boostOfStatus = >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, errors.New("status is not visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the status faveable?
|
|
||||||
if targetStatus.VisibilityAdvanced != nil {
|
|
||||||
if !targetStatus.VisibilityAdvanced.Likeable {
|
|
||||||
return nil, errors.New("status is not faveable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// first check if the status is already faved, if so we don't need to do anything
|
|
||||||
newFave := true
|
|
||||||
gtsFave := >smodel.Status{}
|
|
||||||
if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: authed.Account.ID}}, gtsFave); err == nil {
|
|
||||||
// we already have a fave for this status
|
|
||||||
newFave = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if newFave {
|
|
||||||
thisFaveID := uuid.NewString()
|
|
||||||
|
|
||||||
// we need to create a new fave in the database
|
|
||||||
gtsFave := >smodel.StatusFave{
|
|
||||||
ID: thisFaveID,
|
|
||||||
AccountID: authed.Account.ID,
|
|
||||||
TargetAccountID: targetAccount.ID,
|
|
||||||
StatusID: targetStatus.ID,
|
|
||||||
URI: util.GenerateURIForLike(authed.Account.Username, p.config.Protocol, p.config.Host, thisFaveID),
|
|
||||||
GTSStatus: targetStatus,
|
|
||||||
GTSTargetAccount: targetAccount,
|
|
||||||
GTSFavingAccount: authed.Account,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.db.Put(gtsFave); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// send the new fave through the processor channel for federation etc
|
|
||||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
|
||||||
APObjectType: gtsmodel.ActivityStreamsLike,
|
|
||||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
|
||||||
GTSModel: gtsFave,
|
|
||||||
OriginAccount: authed.Account,
|
|
||||||
TargetAccount: targetAccount,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the mastodon representation of the target status
|
|
||||||
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, ErrorWithCode) {
|
func (p *processor) StatusBoost(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
l := p.log.WithField("func", "StatusBoost")
|
return p.statusProcessor.Boost(authed.Account, authed.Application, targetStatusID)
|
||||||
|
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, NewErrorNotFound(errors.New("status is not visible"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if targetStatus.VisibilityAdvanced != nil {
|
|
||||||
if !targetStatus.VisibilityAdvanced.Boostable {
|
|
||||||
return nil, NewErrorForbidden(errors.New("status is not boostable"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's visible! it's boostable! so let's boost the FUCK out of it
|
|
||||||
boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, authed.Account)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
boostWrapperStatus.CreatedWithApplicationID = authed.Application.ID
|
|
||||||
boostWrapperStatus.GTSBoostedAccount = targetAccount
|
|
||||||
|
|
||||||
// put the boost in the database
|
|
||||||
if err := p.db.Put(boostWrapperStatus); err != nil {
|
|
||||||
return nil, NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// send it to the processor for async processing
|
|
||||||
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
|
||||||
APObjectType: gtsmodel.ActivityStreamsAnnounce,
|
|
||||||
APActivityType: gtsmodel.ActivityStreamsCreate,
|
|
||||||
GTSModel: boostWrapperStatus,
|
|
||||||
OriginAccount: authed.Account,
|
|
||||||
TargetAccount: targetAccount,
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the frontend representation of the new status to the submitter
|
|
||||||
mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, authed.Account, authed.Account, targetAccount, nil, targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, ErrorWithCode) {
|
func (p *processor) StatusBoostedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
l := p.log.WithField("func", "StatusBoostedBy")
|
return p.statusProcessor.BoostedBy(authed.Account, targetStatusID)
|
||||||
|
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching status %s: %s", targetStatusID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching related accounts for status %s: %s", targetStatusID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
|
|
||||||
favingAccounts, err := p.db.WhoBoostedStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter the list so the user doesn't see accounts they blocked or which blocked them
|
|
||||||
filteredAccounts := []*gtsmodel.Account{}
|
|
||||||
for _, acc := range favingAccounts {
|
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, acc.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))
|
|
||||||
}
|
|
||||||
if !blocked {
|
|
||||||
filteredAccounts = append(filteredAccounts, acc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: filter other things here? suspended? muted? silenced?
|
|
||||||
|
|
||||||
// now we can return the masto representation of those accounts
|
|
||||||
mastoAccounts := []*apimodel.Account{}
|
|
||||||
for _, acc := range filteredAccounts {
|
|
||||||
mastoAccount, err := p.tc.AccountToMastoPublic(acc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
|
|
||||||
}
|
|
||||||
mastoAccounts = append(mastoAccounts, mastoAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoAccounts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
|
func (p *processor) StatusFavedBy(authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, error) {
|
||||||
l := p.log.WithField("func", "StatusFavedBy")
|
return p.statusProcessor.FavedBy(authed.Account, targetStatusID)
|
||||||
|
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, errors.New("status is not visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
|
|
||||||
favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error seeing who faved status: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter the list so the user doesn't see accounts they blocked or which blocked them
|
|
||||||
filteredAccounts := []*gtsmodel.Account{}
|
|
||||||
for _, acc := range favingAccounts {
|
|
||||||
blocked, err := p.db.Blocked(authed.Account.ID, acc.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error checking blocks: %s", err)
|
|
||||||
}
|
|
||||||
if !blocked {
|
|
||||||
filteredAccounts = append(filteredAccounts, acc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: filter other things here? suspended? muted? silenced?
|
|
||||||
|
|
||||||
// now we can return the masto representation of those accounts
|
|
||||||
mastoAccounts := []*apimodel.Account{}
|
|
||||||
for _, acc := range filteredAccounts {
|
|
||||||
mastoAccount, err := p.tc.AccountToMastoPublic(acc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting account to api model: %s", err)
|
|
||||||
}
|
|
||||||
mastoAccounts = append(mastoAccounts, mastoAccount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoAccounts, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
func (p *processor) StatusGet(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
||||||
l := p.log.WithField("func", "StatusGet")
|
return p.statusProcessor.Get(authed.Account, targetStatusID)
|
||||||
|
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, errors.New("status is not visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
var boostOfStatus *gtsmodel.Status
|
|
||||||
if targetStatus.BoostOfID != "" {
|
|
||||||
boostOfStatus = >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoStatus, nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
func (p *processor) StatusUnfave(authed *oauth.Auth, targetStatusID string) (*apimodel.Status, error) {
|
||||||
l := p.log.WithField("func", "StatusUnfave")
|
return p.statusProcessor.Unfave(authed.Account, targetStatusID)
|
||||||
l.Tracef("going to search for target status %s", targetStatusID)
|
|
||||||
targetStatus := >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
|
||||||
targetAccount := >smodel.Account{}
|
|
||||||
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to get relevant accounts")
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Trace("going to see if status is visible")
|
|
||||||
visible, err := p.db.StatusVisible(targetStatus, targetAccount, authed.Account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
return nil, errors.New("status is not visible")
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the status faveable?
|
|
||||||
if targetStatus.VisibilityAdvanced != nil {
|
|
||||||
if !targetStatus.VisibilityAdvanced.Likeable {
|
|
||||||
return nil, errors.New("status is not faveable")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// it's visible! it's faveable! so let's unfave the FUCK out of it
|
|
||||||
_, err = p.db.UnfaveStatus(targetStatus, authed.Account.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error unfaveing status: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var boostOfStatus *gtsmodel.Status
|
|
||||||
if targetStatus.BoostOfID != "" {
|
|
||||||
boostOfStatus = >smodel.Status{}
|
|
||||||
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, authed.Account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mastoStatus, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, ErrorWithCode) {
|
func (p *processor) StatusGetContext(authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
||||||
return &apimodel.Context{}, nil
|
return p.statusProcessor.Context(authed.Account, targetStatusID)
|
||||||
}
|
}
|
||||||
|
|
79
internal/processing/synchronous/status/boost.go
Normal file
79
internal/processing/synchronous/status/boost.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusBoost")
|
||||||
|
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetStatus.VisibilityAdvanced != nil {
|
||||||
|
if !targetStatus.VisibilityAdvanced.Boostable {
|
||||||
|
return nil, gtserror.NewErrorForbidden(errors.New("status is not boostable"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's visible! it's boostable! so let's boost the FUCK out of it
|
||||||
|
boostWrapperStatus, err := p.tc.StatusToBoost(targetStatus, account)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boostWrapperStatus.CreatedWithApplicationID = application.ID
|
||||||
|
boostWrapperStatus.GTSBoostedAccount = targetAccount
|
||||||
|
|
||||||
|
// put the boost in the database
|
||||||
|
if err := p.db.Put(boostWrapperStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsAnnounce,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: boostWrapperStatus,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the frontend representation of the new status to the submitter
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(boostWrapperStatus, account, account, targetAccount, nil, targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
74
internal/processing/synchronous/status/boostedby.go
Normal file
74
internal/processing/synchronous/status/boostedby.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusBoostedBy")
|
||||||
|
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("StatusBoostedBy: status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
|
||||||
|
favingAccounts, err := p.db.WhoBoostedStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error seeing who boosted status: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter the list so the user doesn't see accounts they blocked or which blocked them
|
||||||
|
filteredAccounts := []*gtsmodel.Account{}
|
||||||
|
for _, acc := range favingAccounts {
|
||||||
|
blocked, err := p.db.Blocked(account.ID, acc.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusBoostedBy: error checking blocks: %s", err))
|
||||||
|
}
|
||||||
|
if !blocked {
|
||||||
|
filteredAccounts = append(filteredAccounts, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: filter other things here? suspended? muted? silenced?
|
||||||
|
|
||||||
|
// now we can return the masto representation of those accounts
|
||||||
|
mastoAccounts := []*apimodel.Account{}
|
||||||
|
for _, acc := range filteredAccounts {
|
||||||
|
mastoAccount, err := p.tc.AccountToMastoPublic(acc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("StatusFavedBy: error converting account to api model: %s", err))
|
||||||
|
}
|
||||||
|
mastoAccounts = append(mastoAccounts, mastoAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoAccounts, nil
|
||||||
|
}
|
14
internal/processing/synchronous/status/context.go
Normal file
14
internal/processing/synchronous/status/context.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
||||||
|
return &apimodel.Context{
|
||||||
|
Ancestors: []apimodel.Status{},
|
||||||
|
Descendants: []apimodel.Status{},
|
||||||
|
}, nil
|
||||||
|
}
|
105
internal/processing/synchronous/status/create.go
Normal file
105
internal/processing/synchronous/status/create.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
uris := util.GenerateURIsForAccount(account.Username, p.config.Protocol, p.config.Host)
|
||||||
|
thisStatusID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
thisStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, thisStatusID)
|
||||||
|
thisStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, thisStatusID)
|
||||||
|
|
||||||
|
newStatus := >smodel.Status{
|
||||||
|
ID: thisStatusID,
|
||||||
|
URI: thisStatusURI,
|
||||||
|
URL: thisStatusURL,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
Local: true,
|
||||||
|
AccountID: account.ID,
|
||||||
|
ContentWarning: form.SpoilerText,
|
||||||
|
ActivityStreamsType: gtsmodel.ActivityStreamsNote,
|
||||||
|
Sensitive: form.Sensitive,
|
||||||
|
Language: form.Language,
|
||||||
|
CreatedWithApplicationID: application.ID,
|
||||||
|
Text: form.Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if replyToID is ok
|
||||||
|
if err := p.processReplyToID(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if mediaIDs are ok
|
||||||
|
if err := p.processMediaIDs(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if visibility settings are ok
|
||||||
|
if err := p.processVisibility(form, account.Privacy, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle language settings
|
||||||
|
if err := p.processLanguage(form, account.Language, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle mentions
|
||||||
|
if err := p.processMentions(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.processTags(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.processEmojis(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.processContent(form, account.ID, newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// put the new status in the database, generating an ID for it in the process
|
||||||
|
if err := p.db.Put(newStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change the status ID of the media attachments to the new status
|
||||||
|
for _, a := range newStatus.GTSMediaAttachments {
|
||||||
|
a.StatusID = newStatus.ID
|
||||||
|
a.UpdatedAt = time.Now()
|
||||||
|
if err := p.db.UpdateByID(a.ID, a); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: newStatus,
|
||||||
|
OriginAccount: account,
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the frontend representation of the new status to the submitter
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(newStatus, account, account, nil, newStatus.GTSReplyToAccount, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", newStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
61
internal/processing/synchronous/status/delete.go
Normal file
61
internal/processing/synchronous/status/delete.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusDelete")
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
// status is already gone
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetStatus.AccountID != account.ID {
|
||||||
|
return nil, gtserror.NewErrorForbidden(errors.New("status doesn't belong to requesting account"))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var boostOfStatus *gtsmodel.Status
|
||||||
|
if targetStatus.BoostOfID != "" {
|
||||||
|
boostOfStatus = >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(targetStatus, account, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.db.DeleteByID(targetStatus.ID, >smodel.Status{}); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error deleting status from the database: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsNote,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsDelete,
|
||||||
|
GTSModel: targetStatus,
|
||||||
|
OriginAccount: account,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
107
internal/processing/synchronous/status/fave.go
Normal file
107
internal/processing/synchronous/status/fave.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusFave")
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var boostOfStatus *gtsmodel.Status
|
||||||
|
if targetStatus.BoostOfID != "" {
|
||||||
|
boostOfStatus = >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the status faveable?
|
||||||
|
if targetStatus.VisibilityAdvanced != nil {
|
||||||
|
if !targetStatus.VisibilityAdvanced.Likeable {
|
||||||
|
return nil, gtserror.NewErrorForbidden(errors.New("status is not faveable"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// first check if the status is already faved, if so we don't need to do anything
|
||||||
|
newFave := true
|
||||||
|
gtsFave := >smodel.StatusFave{}
|
||||||
|
if err := p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err == nil {
|
||||||
|
// we already have a fave for this status
|
||||||
|
newFave = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if newFave {
|
||||||
|
thisFaveID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to create a new fave in the database
|
||||||
|
gtsFave := >smodel.StatusFave{
|
||||||
|
ID: thisFaveID,
|
||||||
|
AccountID: account.ID,
|
||||||
|
TargetAccountID: targetAccount.ID,
|
||||||
|
StatusID: targetStatus.ID,
|
||||||
|
URI: util.GenerateURIForLike(account.Username, p.config.Protocol, p.config.Host, thisFaveID),
|
||||||
|
GTSStatus: targetStatus,
|
||||||
|
GTSTargetAccount: targetAccount,
|
||||||
|
GTSFavingAccount: account,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.db.Put(gtsFave); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting fave in database: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsLike,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsCreate,
|
||||||
|
GTSModel: gtsFave,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the mastodon representation of the target status
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
74
internal/processing/synchronous/status/favedby.go
Normal file
74
internal/processing/synchronous/status/favedby.go
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusFavedBy")
|
||||||
|
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// get ALL accounts that faved a status -- doesn't take account of blocks and mutes and stuff
|
||||||
|
favingAccounts, err := p.db.WhoFavedStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter the list so the user doesn't see accounts they blocked or which blocked them
|
||||||
|
filteredAccounts := []*gtsmodel.Account{}
|
||||||
|
for _, acc := range favingAccounts {
|
||||||
|
blocked, err := p.db.Blocked(account.ID, acc.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err))
|
||||||
|
}
|
||||||
|
if !blocked {
|
||||||
|
filteredAccounts = append(filteredAccounts, acc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: filter other things here? suspended? muted? silenced?
|
||||||
|
|
||||||
|
// now we can return the masto representation of those accounts
|
||||||
|
mastoAccounts := []*apimodel.Account{}
|
||||||
|
for _, acc := range filteredAccounts {
|
||||||
|
mastoAccount, err := p.tc.AccountToMastoPublic(acc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
mastoAccounts = append(mastoAccounts, mastoAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoAccounts, nil
|
||||||
|
}
|
58
internal/processing/synchronous/status/get.go
Normal file
58
internal/processing/synchronous/status/get.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusGet")
|
||||||
|
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var boostOfStatus *gtsmodel.Status
|
||||||
|
if targetStatus.BoostOfID != "" {
|
||||||
|
boostOfStatus = >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
|
||||||
|
}
|
52
internal/processing/synchronous/status/status.go
Normal file
52
internal/processing/synchronous/status/status.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Processor wraps a bunch of functions for processing statuses.
|
||||||
|
type Processor interface {
|
||||||
|
// Create processes the given form to create a new status, returning the api model representation of that status if it's OK.
|
||||||
|
Create(account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Delete processes the delete of a given status, returning the deleted status if the delete goes through.
|
||||||
|
Delete(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Fave processes the faving of a given status, returning the updated status if the fave goes through.
|
||||||
|
Fave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well.
|
||||||
|
Boost(account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
|
BoostedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
||||||
|
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
|
FavedBy(account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode)
|
||||||
|
// Get gets the given status, taking account of privacy settings and blocks etc.
|
||||||
|
Get(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Unfave processes the unfaving of a given status, returning the updated status if the fave goes through.
|
||||||
|
Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode)
|
||||||
|
// Context returns the context (previous and following posts) from the given status ID
|
||||||
|
Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
type processor struct {
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
config *config.Config
|
||||||
|
db db.DB
|
||||||
|
fromClientAPI chan gtsmodel.FromClientAPI
|
||||||
|
log *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new status processor.
|
||||||
|
func New(db db.DB, tc typeutils.TypeConverter, config *config.Config, fromClientAPI chan gtsmodel.FromClientAPI, log *logrus.Logger) Processor {
|
||||||
|
return &processor{
|
||||||
|
tc: tc,
|
||||||
|
config: config,
|
||||||
|
db: db,
|
||||||
|
fromClientAPI: fromClientAPI,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
92
internal/processing/synchronous/status/unfave.go
Normal file
92
internal/processing/synchronous/status/unfave.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) Unfave(account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
l := p.log.WithField("func", "StatusUnfave")
|
||||||
|
l.Tracef("going to search for target status %s", targetStatusID)
|
||||||
|
targetStatus := >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatusID, targetStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Tracef("going to search for target account %s", targetStatus.AccountID)
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(targetStatus.AccountID, targetAccount); err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching target account %s: %s", targetStatus.AccountID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to get relevant accounts")
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(targetStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching related accounts for status %s: %s", targetStatusID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace("going to see if status is visible")
|
||||||
|
visible, err := p.db.StatusVisible(targetStatus, account, relevantAccounts) // requestingAccount might well be nil here, but StatusVisible knows how to take care of that
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !visible {
|
||||||
|
return nil, gtserror.NewErrorNotFound(errors.New("status is not visible"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we actually have a fave for this status
|
||||||
|
var toUnfave bool
|
||||||
|
|
||||||
|
gtsFave := >smodel.StatusFave{}
|
||||||
|
err = p.db.GetWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave)
|
||||||
|
if err == nil {
|
||||||
|
// we have a fave
|
||||||
|
toUnfave = true
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
// something went wrong in the db finding the fave
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err))
|
||||||
|
}
|
||||||
|
// we just don't have a fave
|
||||||
|
toUnfave = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if toUnfave {
|
||||||
|
// we had a fave, so take some action to get rid of it
|
||||||
|
if err := p.db.DeleteWhere([]db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: account.ID}}, gtsFave); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// send it back to the processor for async processing
|
||||||
|
p.fromClientAPI <- gtsmodel.FromClientAPI{
|
||||||
|
APObjectType: gtsmodel.ActivityStreamsLike,
|
||||||
|
APActivityType: gtsmodel.ActivityStreamsUndo,
|
||||||
|
GTSModel: gtsFave,
|
||||||
|
OriginAccount: account,
|
||||||
|
TargetAccount: targetAccount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the status (whatever its state) back to the caller
|
||||||
|
var boostOfStatus *gtsmodel.Status
|
||||||
|
if targetStatus.BoostOfID != "" {
|
||||||
|
boostOfStatus = >smodel.Status{}
|
||||||
|
if err := p.db.GetByID(targetStatus.BoostOfID, boostOfStatus); err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching boosted status %s: %s", targetStatus.BoostOfID, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mastoStatus, err := p.tc.StatusToMasto(targetStatus, targetAccount, account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, boostOfStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastoStatus, nil
|
||||||
|
}
|
269
internal/processing/synchronous/status/util.go
Normal file
269
internal/processing/synchronous/status/util.go
Normal file
|
@ -0,0 +1,269 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
|
||||||
|
// by default all flags are set to true
|
||||||
|
gtsAdvancedVis := >smodel.VisibilityAdvanced{
|
||||||
|
Federated: true,
|
||||||
|
Boostable: true,
|
||||||
|
Replyable: true,
|
||||||
|
Likeable: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var gtsBasicVis gtsmodel.Visibility
|
||||||
|
// Advanced takes priority if it's set.
|
||||||
|
// If it's not set, take whatever masto visibility is set.
|
||||||
|
// If *that's* not set either, then just take the account default.
|
||||||
|
// If that's also not set, take the default for the whole instance.
|
||||||
|
if form.VisibilityAdvanced != nil {
|
||||||
|
gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
|
||||||
|
} else if form.Visibility != "" {
|
||||||
|
gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
|
||||||
|
} else if accountDefaultVis != "" {
|
||||||
|
gtsBasicVis = accountDefaultVis
|
||||||
|
} else {
|
||||||
|
gtsBasicVis = gtsmodel.VisibilityDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
switch gtsBasicVis {
|
||||||
|
case gtsmodel.VisibilityPublic:
|
||||||
|
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
|
||||||
|
break
|
||||||
|
case gtsmodel.VisibilityUnlocked:
|
||||||
|
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
|
||||||
|
if form.Federated != nil {
|
||||||
|
gtsAdvancedVis.Federated = *form.Federated
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Boostable != nil {
|
||||||
|
gtsAdvancedVis.Boostable = *form.Boostable
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Replyable != nil {
|
||||||
|
gtsAdvancedVis.Replyable = *form.Replyable
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Likeable != nil {
|
||||||
|
gtsAdvancedVis.Likeable = *form.Likeable
|
||||||
|
}
|
||||||
|
|
||||||
|
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
|
||||||
|
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
|
||||||
|
gtsAdvancedVis.Boostable = false
|
||||||
|
|
||||||
|
if form.Federated != nil {
|
||||||
|
gtsAdvancedVis.Federated = *form.Federated
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Replyable != nil {
|
||||||
|
gtsAdvancedVis.Replyable = *form.Replyable
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Likeable != nil {
|
||||||
|
gtsAdvancedVis.Likeable = *form.Likeable
|
||||||
|
}
|
||||||
|
|
||||||
|
case gtsmodel.VisibilityDirect:
|
||||||
|
// direct is pretty easy: there's only one possible setting so return it
|
||||||
|
gtsAdvancedVis.Federated = true
|
||||||
|
gtsAdvancedVis.Boostable = false
|
||||||
|
gtsAdvancedVis.Federated = true
|
||||||
|
gtsAdvancedVis.Likeable = true
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Visibility = gtsBasicVis
|
||||||
|
status.VisibilityAdvanced = gtsAdvancedVis
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
||||||
|
if form.InReplyToID == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
|
||||||
|
//
|
||||||
|
// 1. Does the replied status exist in the database?
|
||||||
|
// 2. Is the replied status marked as replyable?
|
||||||
|
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
|
||||||
|
//
|
||||||
|
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
|
||||||
|
repliedStatus := >smodel.Status{}
|
||||||
|
repliedAccount := >smodel.Account{}
|
||||||
|
// check replied status exists + is replyable
|
||||||
|
if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if repliedStatus.VisibilityAdvanced != nil {
|
||||||
|
if !repliedStatus.VisibilityAdvanced.Replyable {
|
||||||
|
return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check replied account is known to us
|
||||||
|
if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||||
|
}
|
||||||
|
// check if a block exists
|
||||||
|
if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
||||||
|
}
|
||||||
|
} else if blocked {
|
||||||
|
return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
|
||||||
|
}
|
||||||
|
status.InReplyToID = repliedStatus.ID
|
||||||
|
status.InReplyToAccountID = repliedAccount.ID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
||||||
|
if form.MediaIDs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
|
||||||
|
attachments := []string{}
|
||||||
|
for _, mediaID := range form.MediaIDs {
|
||||||
|
// check these attachments exist
|
||||||
|
a := >smodel.MediaAttachment{}
|
||||||
|
if err := p.db.GetByID(mediaID, a); err != nil {
|
||||||
|
return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
|
||||||
|
}
|
||||||
|
// check they belong to the requesting account id
|
||||||
|
if a.AccountID != thisAccountID {
|
||||||
|
return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
|
||||||
|
}
|
||||||
|
// check they're not already used in a status
|
||||||
|
if a.StatusID != "" || a.ScheduledStatusID != "" {
|
||||||
|
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
|
||||||
|
}
|
||||||
|
gtsMediaAttachments = append(gtsMediaAttachments, a)
|
||||||
|
attachments = append(attachments, a.ID)
|
||||||
|
}
|
||||||
|
status.GTSMediaAttachments = gtsMediaAttachments
|
||||||
|
status.Attachments = attachments
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
|
||||||
|
if form.Language != "" {
|
||||||
|
status.Language = form.Language
|
||||||
|
} else {
|
||||||
|
status.Language = accountDefaultLanguage
|
||||||
|
}
|
||||||
|
if status.Language == "" {
|
||||||
|
return errors.New("no language given either in status create form or account default")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||||
|
menchies := []string{}
|
||||||
|
gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating mentions from status: %s", err)
|
||||||
|
}
|
||||||
|
for _, menchie := range gtsMenchies {
|
||||||
|
menchieID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
menchie.ID = menchieID
|
||||||
|
|
||||||
|
if err := p.db.Put(menchie); err != nil {
|
||||||
|
return fmt.Errorf("error putting mentions in db: %s", err)
|
||||||
|
}
|
||||||
|
menchies = append(menchies, menchie.ID)
|
||||||
|
}
|
||||||
|
// add full populated gts menchies to the status for passing them around conveniently
|
||||||
|
status.GTSMentions = gtsMenchies
|
||||||
|
// add just the ids of the mentioned accounts to the status for putting in the db
|
||||||
|
status.Mentions = menchies
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||||
|
tags := []string{}
|
||||||
|
gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating hashtags from status: %s", err)
|
||||||
|
}
|
||||||
|
for _, tag := range gtsTags {
|
||||||
|
if err := p.db.Upsert(tag, "name"); err != nil {
|
||||||
|
return fmt.Errorf("error putting tags in db: %s", err)
|
||||||
|
}
|
||||||
|
tags = append(tags, tag.ID)
|
||||||
|
}
|
||||||
|
// add full populated gts tags to the status for passing them around conveniently
|
||||||
|
status.GTSTags = gtsTags
|
||||||
|
// add just the ids of the used tags to the status for putting in the db
|
||||||
|
status.Tags = tags
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||||
|
emojis := []string{}
|
||||||
|
gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error generating emojis from status: %s", err)
|
||||||
|
}
|
||||||
|
for _, e := range gtsEmojis {
|
||||||
|
emojis = append(emojis, e.ID)
|
||||||
|
}
|
||||||
|
// add full populated gts emojis to the status for passing them around conveniently
|
||||||
|
status.GTSEmojis = gtsEmojis
|
||||||
|
// add just the ids of the used emojis to the status for putting in the db
|
||||||
|
status.Emojis = emojis
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) processContent(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
||||||
|
if form.Status == "" {
|
||||||
|
status.Content = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// surround the whole status in '<p>'
|
||||||
|
content := fmt.Sprintf(`<p>%s</p>`, form.Status)
|
||||||
|
|
||||||
|
// format mentions nicely
|
||||||
|
for _, menchie := range status.GTSMentions {
|
||||||
|
targetAccount := >smodel.Account{}
|
||||||
|
if err := p.db.GetByID(menchie.TargetAccountID, targetAccount); err == nil {
|
||||||
|
mentionContent := fmt.Sprintf(`<span class="h-card"><a href="%s" class="u-url mention">@<span>%s</span></a></span>`, targetAccount.URL, targetAccount.Username)
|
||||||
|
content = strings.ReplaceAll(content, menchie.NameString, mentionContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// format tags nicely
|
||||||
|
for _, tag := range status.GTSTags {
|
||||||
|
tagContent := fmt.Sprintf(`<a href="%s" class="mention hashtag" rel="tag">#<span>%s</span></a>`, tag.URL, tag.Name)
|
||||||
|
content = strings.ReplaceAll(content, fmt.Sprintf("#%s", tag.Name), tagContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace newlines with breaks
|
||||||
|
content = strings.ReplaceAll(content, "\n", "<br />")
|
||||||
|
|
||||||
|
status.Content = content
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -20,45 +20,70 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode) {
|
func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) {
|
||||||
statuses, err := p.db.GetHomeTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
|
resp := &apimodel.StatusTimelineResponse{
|
||||||
if err != nil {
|
Statuses: []*apimodel.Status{},
|
||||||
return nil, NewErrorInternalError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := p.filterStatuses(authed, statuses)
|
apiStatuses, err := p.timelineManager.HomeTimeline(authed.Account.ID, maxID, sinceID, minID, limit, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
resp.Statuses = apiStatuses
|
||||||
|
|
||||||
|
// prepare the next and previous links
|
||||||
|
if len(apiStatuses) != 0 {
|
||||||
|
nextLink := &url.URL{
|
||||||
|
Scheme: p.config.Protocol,
|
||||||
|
Host: p.config.Host,
|
||||||
|
Path: "/api/v1/timelines/home",
|
||||||
|
RawPath: url.PathEscape("api/v1/timelines/home"),
|
||||||
|
RawQuery: fmt.Sprintf("limit=%d&max_id=%s", limit, apiStatuses[len(apiStatuses)-1].ID),
|
||||||
|
}
|
||||||
|
next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String())
|
||||||
|
|
||||||
|
prevLink := &url.URL{
|
||||||
|
Scheme: p.config.Protocol,
|
||||||
|
Host: p.config.Host,
|
||||||
|
Path: "/api/v1/timelines/home",
|
||||||
|
RawQuery: fmt.Sprintf("limit=%d&min_id=%s", limit, apiStatuses[0].ID),
|
||||||
|
}
|
||||||
|
prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String())
|
||||||
|
resp.LinkHeader = fmt.Sprintf("%s, %s", next, prev)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]apimodel.Status, ErrorWithCode) {
|
func (p *processor) PublicTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, gtserror.WithCode) {
|
||||||
statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
|
statuses, err := p.db.GetPublicTimelineForAccount(authed.Account.ID, maxID, sinceID, minID, limit, local)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := p.filterStatuses(authed, statuses)
|
s, err := p.filterStatuses(authed, statuses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]apimodel.Status, error) {
|
func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) {
|
||||||
l := p.log.WithField("func", "filterStatuses")
|
l := p.log.WithField("func", "filterStatuses")
|
||||||
|
|
||||||
apiStatuses := []apimodel.Status{}
|
apiStatuses := []*apimodel.Status{}
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
targetAccount := >smodel.Account{}
|
targetAccount := >smodel.Account{}
|
||||||
if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
|
if err := p.db.GetByID(s.AccountID, targetAccount); err != nil {
|
||||||
|
@ -66,7 +91,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
|
||||||
l.Debugf("skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
|
l.Debugf("skipping status %s because account %s can't be found in the db", s.ID, s.AccountID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting status author: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
|
||||||
|
@ -75,9 +100,9 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
visible, err := p.db.StatusVisible(s, targetAccount, authed.Account, relevantAccounts)
|
visible, err := p.db.StatusVisible(s, authed.Account, relevantAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking status visibility: %s", err))
|
||||||
}
|
}
|
||||||
if !visible {
|
if !visible {
|
||||||
continue
|
continue
|
||||||
|
@ -91,7 +116,7 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
|
||||||
l.Debugf("skipping status %s because status %s can't be found in the db", s.ID, s.BoostOfID)
|
l.Debugf("skipping status %s because status %s can't be found in the db", s.ID, s.BoostOfID)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error getting boosted status: %s", err))
|
||||||
}
|
}
|
||||||
boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
|
boostedRelevantAccounts, err := p.db.PullRelevantAccountsFromStatus(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -99,9 +124,9 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
boostedVisible, err := p.db.StatusVisible(bs, relevantAccounts.BoostedAccount, authed.Account, boostedRelevantAccounts)
|
boostedVisible, err := p.db.StatusVisible(bs, authed.Account, boostedRelevantAccounts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err))
|
return nil, gtserror.NewErrorInternalError(fmt.Errorf("HomeTimelineGet: error checking boosted status visibility: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if boostedVisible {
|
if boostedVisible {
|
||||||
|
@ -115,8 +140,113 @@ func (p *processor) filterStatuses(authed *oauth.Auth, statuses []*gtsmodel.Stat
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatuses = append(apiStatuses, *apiStatus)
|
apiStatuses = append(apiStatuses, apiStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiStatuses, nil
|
return apiStatuses, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *processor) initTimelines() error {
|
||||||
|
// get all local accounts (ie., domain = nil) that aren't suspended (suspended_at = nil)
|
||||||
|
localAccounts := []*gtsmodel.Account{}
|
||||||
|
where := []db.Where{
|
||||||
|
{
|
||||||
|
Key: "domain", Value: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "suspended_at", Value: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := p.db.GetWhere(where, &localAccounts); err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("initTimelines: db error initializing timelines: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to wait until all timelines are populated so created a waitgroup here
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(localAccounts))
|
||||||
|
|
||||||
|
for _, localAccount := range localAccounts {
|
||||||
|
// to save time we can populate the timelines asynchronously
|
||||||
|
// this will go heavy on the database, but since we're not actually serving yet it doesn't really matter
|
||||||
|
go p.initTimelineFor(localAccount, wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for all timelines to be populated before we exit
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) initTimelineFor(account *gtsmodel.Account, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
l := p.log.WithFields(logrus.Fields{
|
||||||
|
"func": "initTimelineFor",
|
||||||
|
"accountID": account.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
desiredIndexLength := p.timelineManager.GetDesiredIndexLength()
|
||||||
|
|
||||||
|
statuses, err := p.db.GetStatusesWhereFollowing(account.ID, "", "", "", desiredIndexLength, false)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error getting statuses: %s", err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.indexAndIngest(statuses, account, desiredIndexLength)
|
||||||
|
|
||||||
|
lengthNow := p.timelineManager.GetIndexedLength(account.ID)
|
||||||
|
if lengthNow < desiredIndexLength {
|
||||||
|
// try and get more posts from the last ID onwards
|
||||||
|
rearmostStatusID, err := p.timelineManager.GetOldestIndexedID(account.ID)
|
||||||
|
if err != nil {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error getting id of rearmost status: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rearmostStatusID != "" {
|
||||||
|
moreStatuses, err := p.db.GetStatusesWhereFollowing(account.ID, rearmostStatusID, "", "", desiredIndexLength/2, false)
|
||||||
|
if err != nil {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error getting more statuses: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.indexAndIngest(moreStatuses, account, desiredIndexLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debugf("prepared timeline of length %d for account %s", lengthNow, account.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *processor) indexAndIngest(statuses []*gtsmodel.Status, timelineAccount *gtsmodel.Account, desiredIndexLength int) {
|
||||||
|
l := p.log.WithFields(logrus.Fields{
|
||||||
|
"func": "indexAndIngest",
|
||||||
|
"accountID": timelineAccount.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, s := range statuses {
|
||||||
|
relevantAccounts, err := p.db.PullRelevantAccountsFromStatus(s)
|
||||||
|
if err != nil {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error getting relevant accounts from status %s: %s", s.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visible, err := p.db.StatusVisible(s, timelineAccount, relevantAccounts)
|
||||||
|
if err != nil {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error checking visibility of status %s: %s", s.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if visible {
|
||||||
|
if err := p.timelineManager.Ingest(s, timelineAccount.ID); err != nil {
|
||||||
|
l.Error(fmt.Errorf("initTimelineFor: error ingesting status %s: %s", s.ID, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have enough posts now and return if we do
|
||||||
|
if p.timelineManager.GetIndexedLength(timelineAccount.ID) >= desiredIndexLength {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,233 +25,11 @@
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *processor) processVisibility(form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error {
|
|
||||||
// by default all flags are set to true
|
|
||||||
gtsAdvancedVis := >smodel.VisibilityAdvanced{
|
|
||||||
Federated: true,
|
|
||||||
Boostable: true,
|
|
||||||
Replyable: true,
|
|
||||||
Likeable: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
var gtsBasicVis gtsmodel.Visibility
|
|
||||||
// Advanced takes priority if it's set.
|
|
||||||
// If it's not set, take whatever masto visibility is set.
|
|
||||||
// If *that's* not set either, then just take the account default.
|
|
||||||
// If that's also not set, take the default for the whole instance.
|
|
||||||
if form.VisibilityAdvanced != nil {
|
|
||||||
gtsBasicVis = gtsmodel.Visibility(*form.VisibilityAdvanced)
|
|
||||||
} else if form.Visibility != "" {
|
|
||||||
gtsBasicVis = p.tc.MastoVisToVis(form.Visibility)
|
|
||||||
} else if accountDefaultVis != "" {
|
|
||||||
gtsBasicVis = accountDefaultVis
|
|
||||||
} else {
|
|
||||||
gtsBasicVis = gtsmodel.VisibilityDefault
|
|
||||||
}
|
|
||||||
|
|
||||||
switch gtsBasicVis {
|
|
||||||
case gtsmodel.VisibilityPublic:
|
|
||||||
// for public, there's no need to change any of the advanced flags from true regardless of what the user filled out
|
|
||||||
break
|
|
||||||
case gtsmodel.VisibilityUnlocked:
|
|
||||||
// for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them
|
|
||||||
if form.Federated != nil {
|
|
||||||
gtsAdvancedVis.Federated = *form.Federated
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Boostable != nil {
|
|
||||||
gtsAdvancedVis.Boostable = *form.Boostable
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Replyable != nil {
|
|
||||||
gtsAdvancedVis.Replyable = *form.Replyable
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Likeable != nil {
|
|
||||||
gtsAdvancedVis.Likeable = *form.Likeable
|
|
||||||
}
|
|
||||||
|
|
||||||
case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly:
|
|
||||||
// for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them
|
|
||||||
gtsAdvancedVis.Boostable = false
|
|
||||||
|
|
||||||
if form.Federated != nil {
|
|
||||||
gtsAdvancedVis.Federated = *form.Federated
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Replyable != nil {
|
|
||||||
gtsAdvancedVis.Replyable = *form.Replyable
|
|
||||||
}
|
|
||||||
|
|
||||||
if form.Likeable != nil {
|
|
||||||
gtsAdvancedVis.Likeable = *form.Likeable
|
|
||||||
}
|
|
||||||
|
|
||||||
case gtsmodel.VisibilityDirect:
|
|
||||||
// direct is pretty easy: there's only one possible setting so return it
|
|
||||||
gtsAdvancedVis.Federated = true
|
|
||||||
gtsAdvancedVis.Boostable = false
|
|
||||||
gtsAdvancedVis.Federated = true
|
|
||||||
gtsAdvancedVis.Likeable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
status.Visibility = gtsBasicVis
|
|
||||||
status.VisibilityAdvanced = gtsAdvancedVis
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processReplyToID(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
|
||||||
if form.InReplyToID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted:
|
|
||||||
//
|
|
||||||
// 1. Does the replied status exist in the database?
|
|
||||||
// 2. Is the replied status marked as replyable?
|
|
||||||
// 3. Does a block exist between either the current account or the account that posted the status it's replying to?
|
|
||||||
//
|
|
||||||
// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing.
|
|
||||||
repliedStatus := >smodel.Status{}
|
|
||||||
repliedAccount := >smodel.Account{}
|
|
||||||
// check replied status exists + is replyable
|
|
||||||
if err := p.db.GetByID(form.InReplyToID, repliedStatus); err != nil {
|
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
|
||||||
return fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if repliedStatus.VisibilityAdvanced != nil {
|
|
||||||
if !repliedStatus.VisibilityAdvanced.Replyable {
|
|
||||||
return fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check replied account is known to us
|
|
||||||
if err := p.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil {
|
|
||||||
if _, ok := err.(db.ErrNoEntries); ok {
|
|
||||||
return fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
|
||||||
}
|
|
||||||
// check if a block exists
|
|
||||||
if blocked, err := p.db.Blocked(thisAccountID, repliedAccount.ID); err != nil {
|
|
||||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
|
||||||
return fmt.Errorf("status with id %s not replyable: %s", form.InReplyToID, err)
|
|
||||||
}
|
|
||||||
} else if blocked {
|
|
||||||
return fmt.Errorf("status with id %s not replyable", form.InReplyToID)
|
|
||||||
}
|
|
||||||
status.InReplyToID = repliedStatus.ID
|
|
||||||
status.InReplyToAccountID = repliedAccount.ID
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processMediaIDs(form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) error {
|
|
||||||
if form.MediaIDs == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gtsMediaAttachments := []*gtsmodel.MediaAttachment{}
|
|
||||||
attachments := []string{}
|
|
||||||
for _, mediaID := range form.MediaIDs {
|
|
||||||
// check these attachments exist
|
|
||||||
a := >smodel.MediaAttachment{}
|
|
||||||
if err := p.db.GetByID(mediaID, a); err != nil {
|
|
||||||
return fmt.Errorf("invalid media type or media not found for media id %s", mediaID)
|
|
||||||
}
|
|
||||||
// check they belong to the requesting account id
|
|
||||||
if a.AccountID != thisAccountID {
|
|
||||||
return fmt.Errorf("media with id %s does not belong to account %s", mediaID, thisAccountID)
|
|
||||||
}
|
|
||||||
// check they're not already used in a status
|
|
||||||
if a.StatusID != "" || a.ScheduledStatusID != "" {
|
|
||||||
return fmt.Errorf("media with id %s is already attached to a status", mediaID)
|
|
||||||
}
|
|
||||||
gtsMediaAttachments = append(gtsMediaAttachments, a)
|
|
||||||
attachments = append(attachments, a.ID)
|
|
||||||
}
|
|
||||||
status.GTSMediaAttachments = gtsMediaAttachments
|
|
||||||
status.Attachments = attachments
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processLanguage(form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error {
|
|
||||||
if form.Language != "" {
|
|
||||||
status.Language = form.Language
|
|
||||||
} else {
|
|
||||||
status.Language = accountDefaultLanguage
|
|
||||||
}
|
|
||||||
if status.Language == "" {
|
|
||||||
return errors.New("no language given either in status create form or account default")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
|
||||||
menchies := []string{}
|
|
||||||
gtsMenchies, err := p.db.MentionStringsToMentions(util.DeriveMentionsFromStatus(form.Status), accountID, status.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error generating mentions from status: %s", err)
|
|
||||||
}
|
|
||||||
for _, menchie := range gtsMenchies {
|
|
||||||
if err := p.db.Put(menchie); err != nil {
|
|
||||||
return fmt.Errorf("error putting mentions in db: %s", err)
|
|
||||||
}
|
|
||||||
menchies = append(menchies, menchie.ID)
|
|
||||||
}
|
|
||||||
// add full populated gts menchies to the status for passing them around conveniently
|
|
||||||
status.GTSMentions = gtsMenchies
|
|
||||||
// add just the ids of the mentioned accounts to the status for putting in the db
|
|
||||||
status.Mentions = menchies
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processTags(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
|
||||||
tags := []string{}
|
|
||||||
gtsTags, err := p.db.TagStringsToTags(util.DeriveHashtagsFromStatus(form.Status), accountID, status.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error generating hashtags from status: %s", err)
|
|
||||||
}
|
|
||||||
for _, tag := range gtsTags {
|
|
||||||
if err := p.db.Upsert(tag, "name"); err != nil {
|
|
||||||
return fmt.Errorf("error putting tags in db: %s", err)
|
|
||||||
}
|
|
||||||
tags = append(tags, tag.ID)
|
|
||||||
}
|
|
||||||
// add full populated gts tags to the status for passing them around conveniently
|
|
||||||
status.GTSTags = gtsTags
|
|
||||||
// add just the ids of the used tags to the status for putting in the db
|
|
||||||
status.Tags = tags
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *processor) processEmojis(form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error {
|
|
||||||
emojis := []string{}
|
|
||||||
gtsEmojis, err := p.db.EmojiStringsToEmojis(util.DeriveEmojisFromStatus(form.Status), accountID, status.ID)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error generating emojis from status: %s", err)
|
|
||||||
}
|
|
||||||
for _, e := range gtsEmojis {
|
|
||||||
emojis = append(emojis, e.ID)
|
|
||||||
}
|
|
||||||
// add full populated gts emojis to the status for passing them around conveniently
|
|
||||||
status.GTSEmojis = gtsEmojis
|
|
||||||
// add just the ids of the used emojis to the status for putting in the db
|
|
||||||
status.Emojis = emojis
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
HELPER FUNCTIONS
|
HELPER FUNCTIONS
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -125,11 +125,13 @@ func New(config *config.Config, logger *logrus.Logger) (Router, error) {
|
||||||
// create the actual engine here -- this is the core request routing handler for gts
|
// create the actual engine here -- this is the core request routing handler for gts
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
engine.Use(cors.New(cors.Config{
|
engine.Use(cors.New(cors.Config{
|
||||||
AllowAllOrigins: true,
|
AllowAllOrigins: true,
|
||||||
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
AllowBrowserExtensions: true,
|
||||||
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
|
AllowMethods: []string{"POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"},
|
||||||
AllowCredentials: false,
|
AllowHeaders: []string{"Origin", "Content-Length", "Content-Type", "Authorization"},
|
||||||
MaxAge: 12 * time.Hour,
|
AllowWebSockets: true,
|
||||||
|
ExposeHeaders: []string{"Link", "X-RateLimit-Reset", "X-RateLimit-Limit", " X-RateLimit-Remaining", "X-Request-Id"},
|
||||||
|
MaxAge: 2 * time.Minute,
|
||||||
}))
|
}))
|
||||||
engine.MaxMultipartMemory = 8 << 20 // 8 MiB
|
engine.MaxMultipartMemory = 8 << 20 // 8 MiB
|
||||||
|
|
||||||
|
|
309
internal/timeline/get.go
Normal file
309
internal/timeline/get.go
Normal file
|
@ -0,0 +1,309 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *timeline) Get(amount int, maxID string, sinceID string, minID string) ([]*apimodel.Status, error) {
|
||||||
|
l := t.log.WithFields(logrus.Fields{
|
||||||
|
"func": "Get",
|
||||||
|
"accountID": t.accountID,
|
||||||
|
})
|
||||||
|
|
||||||
|
var statuses []*apimodel.Status
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// no params are defined to just fetch from the top
|
||||||
|
if maxID == "" && sinceID == "" && minID == "" {
|
||||||
|
statuses, err = t.GetXFromTop(amount)
|
||||||
|
// aysnchronously prepare the next predicted query so it's ready when the user asks for it
|
||||||
|
if len(statuses) != 0 {
|
||||||
|
nextMaxID := statuses[len(statuses)-1].ID
|
||||||
|
go func() {
|
||||||
|
if err := t.prepareNextQuery(amount, nextMaxID, "", ""); err != nil {
|
||||||
|
l.Errorf("error preparing next query: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxID is defined but sinceID isn't so take from behind
|
||||||
|
if maxID != "" && sinceID == "" {
|
||||||
|
statuses, err = t.GetXBehindID(amount, maxID)
|
||||||
|
// aysnchronously prepare the next predicted query so it's ready when the user asks for it
|
||||||
|
if len(statuses) != 0 {
|
||||||
|
nextMaxID := statuses[len(statuses)-1].ID
|
||||||
|
go func() {
|
||||||
|
if err := t.prepareNextQuery(amount, nextMaxID, "", ""); err != nil {
|
||||||
|
l.Errorf("error preparing next query: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxID is defined and sinceID || minID are as well, so take a slice between them
|
||||||
|
if maxID != "" && sinceID != "" {
|
||||||
|
statuses, err = t.GetXBetweenID(amount, maxID, minID)
|
||||||
|
}
|
||||||
|
if maxID != "" && minID != "" {
|
||||||
|
statuses, err = t.GetXBetweenID(amount, maxID, minID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxID isn't defined, but sinceID || minID are, so take x before
|
||||||
|
if maxID == "" && sinceID != "" {
|
||||||
|
statuses, err = t.GetXBeforeID(amount, sinceID, true)
|
||||||
|
}
|
||||||
|
if maxID == "" && minID != "" {
|
||||||
|
statuses, err = t.GetXBeforeID(amount, minID, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) GetXFromTop(amount int) ([]*apimodel.Status, error) {
|
||||||
|
// make a slice of statuses with the length we need to return
|
||||||
|
statuses := make([]*apimodel.Status, 0, amount)
|
||||||
|
|
||||||
|
if t.preparedPosts.data == nil {
|
||||||
|
t.preparedPosts.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we have enough posts prepared to return
|
||||||
|
if t.preparedPosts.data.Len() < amount {
|
||||||
|
if err := t.PrepareFromTop(amount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// work through the prepared posts from the top and return
|
||||||
|
var served int
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXFromTop: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
statuses = append(statuses, entry.prepared)
|
||||||
|
served = served + 1
|
||||||
|
if served >= amount {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) GetXBehindID(amount int, behindID string) ([]*apimodel.Status, error) {
|
||||||
|
// make a slice of statuses with the length we need to return
|
||||||
|
statuses := make([]*apimodel.Status, 0, amount)
|
||||||
|
|
||||||
|
if t.preparedPosts.data == nil {
|
||||||
|
t.preparedPosts.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the modified list until we hit the mark we're looking for
|
||||||
|
var position int
|
||||||
|
var behindIDMark *list.Element
|
||||||
|
|
||||||
|
findMarkLoop:
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
position = position + 1
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.statusID == behindID {
|
||||||
|
behindIDMark = e
|
||||||
|
break findMarkLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we didn't find it, so we need to make sure it's indexed and prepared and then try again
|
||||||
|
if behindIDMark == nil {
|
||||||
|
if err := t.IndexBehind(behindID, amount); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetXBehindID: error indexing behind and including ID %s", behindID)
|
||||||
|
}
|
||||||
|
if err := t.PrepareBehind(behindID, amount); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetXBehindID: error preparing behind and including ID %s", behindID)
|
||||||
|
}
|
||||||
|
oldestID, err := t.OldestPreparedPostID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if oldestID == "" || oldestID == behindID {
|
||||||
|
// there is no oldest prepared post, or the oldest prepared post is still the post we're looking for entries after
|
||||||
|
// this means we should just return the empty statuses slice since we don't have any more posts to offer
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
return t.GetXBehindID(amount, behindID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we have enough posts prepared behind it to return what we're being asked for
|
||||||
|
if t.preparedPosts.data.Len() < amount+position {
|
||||||
|
if err := t.PrepareBehind(behindID, amount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start serving from the entry right after the mark
|
||||||
|
var served int
|
||||||
|
serveloop:
|
||||||
|
for e := behindIDMark.Next(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBehindID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve up to the amount requested
|
||||||
|
statuses = append(statuses, entry.prepared)
|
||||||
|
served = served + 1
|
||||||
|
if served >= amount {
|
||||||
|
break serveloop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) GetXBeforeID(amount int, beforeID string, startFromTop bool) ([]*apimodel.Status, error) {
|
||||||
|
// make a slice of statuses with the length we need to return
|
||||||
|
statuses := make([]*apimodel.Status, 0, amount)
|
||||||
|
|
||||||
|
if t.preparedPosts.data == nil {
|
||||||
|
t.preparedPosts.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the modified list until we hit the mark we're looking for
|
||||||
|
var beforeIDMark *list.Element
|
||||||
|
findMarkLoop:
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBeforeID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.statusID == beforeID {
|
||||||
|
beforeIDMark = e
|
||||||
|
break findMarkLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we didn't find it, so we need to make sure it's indexed and prepared and then try again
|
||||||
|
if beforeIDMark == nil {
|
||||||
|
if err := t.IndexBefore(beforeID, true, amount); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetXBeforeID: error indexing before and including ID %s", beforeID)
|
||||||
|
}
|
||||||
|
if err := t.PrepareBefore(beforeID, true, amount); err != nil {
|
||||||
|
return nil, fmt.Errorf("GetXBeforeID: error preparing before and including ID %s", beforeID)
|
||||||
|
}
|
||||||
|
return t.GetXBeforeID(amount, beforeID, startFromTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
var served int
|
||||||
|
|
||||||
|
if startFromTop {
|
||||||
|
// start serving from the front/top and keep going until we hit mark or get x amount statuses
|
||||||
|
serveloopFromTop:
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBeforeID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.statusID == beforeID {
|
||||||
|
break serveloopFromTop
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve up to the amount requested
|
||||||
|
statuses = append(statuses, entry.prepared)
|
||||||
|
served = served + 1
|
||||||
|
if served >= amount {
|
||||||
|
break serveloopFromTop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !startFromTop {
|
||||||
|
// start serving from the entry right before the mark
|
||||||
|
serveloopFromBottom:
|
||||||
|
for e := beforeIDMark.Prev(); e != nil; e = e.Prev() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBeforeID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve up to the amount requested
|
||||||
|
statuses = append(statuses, entry.prepared)
|
||||||
|
served = served + 1
|
||||||
|
if served >= amount {
|
||||||
|
break serveloopFromBottom
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) GetXBetweenID(amount int, behindID string, beforeID string) ([]*apimodel.Status, error) {
|
||||||
|
// make a slice of statuses with the length we need to return
|
||||||
|
statuses := make([]*apimodel.Status, 0, amount)
|
||||||
|
|
||||||
|
if t.preparedPosts.data == nil {
|
||||||
|
t.preparedPosts.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the modified list until we hit the mark we're looking for
|
||||||
|
var position int
|
||||||
|
var behindIDMark *list.Element
|
||||||
|
findMarkLoop:
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
position = position + 1
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBetweenID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.statusID == behindID {
|
||||||
|
behindIDMark = e
|
||||||
|
break findMarkLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we didn't find it
|
||||||
|
if behindIDMark == nil {
|
||||||
|
return nil, fmt.Errorf("GetXBetweenID: couldn't find status with ID %s", behindID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we have enough posts prepared behind it to return what we're being asked for
|
||||||
|
if t.preparedPosts.data.Len() < amount+position {
|
||||||
|
if err := t.PrepareBehind(behindID, amount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start serving from the entry right after the mark
|
||||||
|
var served int
|
||||||
|
serveloop:
|
||||||
|
for e := behindIDMark.Next(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("GetXBetweenID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.statusID == beforeID {
|
||||||
|
break serveloop
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve up to the amount requested
|
||||||
|
statuses = append(statuses, entry.prepared)
|
||||||
|
served = served + 1
|
||||||
|
if served >= amount {
|
||||||
|
break serveloop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statuses, nil
|
||||||
|
}
|
143
internal/timeline/index.go
Normal file
143
internal/timeline/index.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *timeline) IndexBefore(statusID string, include bool, amount int) error {
|
||||||
|
// filtered := []*gtsmodel.Status{}
|
||||||
|
// offsetStatus := statusID
|
||||||
|
|
||||||
|
// grabloop:
|
||||||
|
// for len(filtered) < amount {
|
||||||
|
// statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, amount, offsetStatus, include, true)
|
||||||
|
// if err != nil {
|
||||||
|
// if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
// return fmt.Errorf("IndexBeforeAndIncluding: error getting statuses from db: %s", err)
|
||||||
|
// }
|
||||||
|
// break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, s := range statuses {
|
||||||
|
// relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s)
|
||||||
|
// if err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// visible, err := t.db.StatusVisible(s, t.account, relevantAccounts)
|
||||||
|
// if err != nil {
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
// if visible {
|
||||||
|
// filtered = append(filtered, s)
|
||||||
|
// }
|
||||||
|
// offsetStatus = s.ID
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, s := range filtered {
|
||||||
|
// if err := t.IndexOne(s.CreatedAt, s.ID); err != nil {
|
||||||
|
// return fmt.Errorf("IndexBeforeAndIncluding: error indexing status with id %s: %s", s.ID, err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) IndexBehind(statusID string, amount int) error {
|
||||||
|
filtered := []*gtsmodel.Status{}
|
||||||
|
offsetStatus := statusID
|
||||||
|
|
||||||
|
grabloop:
|
||||||
|
for len(filtered) < amount {
|
||||||
|
statuses, err := t.db.GetStatusesWhereFollowing(t.accountID, offsetStatus, "", "", amount, false)
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(db.ErrNoEntries); ok {
|
||||||
|
break grabloop // we just don't have enough statuses left in the db so index what we've got and then bail
|
||||||
|
}
|
||||||
|
return fmt.Errorf("IndexBehindAndIncluding: error getting statuses from db: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range statuses {
|
||||||
|
relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(s)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
visible, err := t.db.StatusVisible(s, t.account, relevantAccounts)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if visible {
|
||||||
|
filtered = append(filtered, s)
|
||||||
|
}
|
||||||
|
offsetStatus = s.ID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range filtered {
|
||||||
|
if err := t.IndexOne(s.CreatedAt, s.ID); err != nil {
|
||||||
|
return fmt.Errorf("IndexBehindAndIncluding: error indexing status with id %s: %s", s.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) IndexOneByID(statusID string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) IndexOne(statusCreatedAt time.Time, statusID string) error {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
postIndexEntry := &postIndexEntry{
|
||||||
|
statusID: statusID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.postIndex.insertIndexed(postIndexEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
postIndexEntry := &postIndexEntry{
|
||||||
|
statusID: statusID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.postIndex.insertIndexed(postIndexEntry); err != nil {
|
||||||
|
return fmt.Errorf("IndexAndPrepareOne: error inserting indexed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.prepare(statusID); err != nil {
|
||||||
|
return fmt.Errorf("IndexAndPrepareOne: error preparing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) OldestIndexedPostID() (string, error) {
|
||||||
|
var id string
|
||||||
|
if t.postIndex == nil || t.postIndex.data == nil {
|
||||||
|
// return an empty string if postindex hasn't been initialized yet
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e := t.postIndex.data.Back()
|
||||||
|
|
||||||
|
if e == nil {
|
||||||
|
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return id, errors.New("OldestIndexedPostID: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
return entry.statusID, nil
|
||||||
|
}
|
217
internal/timeline/manager.go
Normal file
217
internal/timeline/manager.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
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 timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
desiredPostIndexLength = 400
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager abstracts functions for creating timelines for multiple accounts, and adding, removing, and fetching entries from those timelines.
|
||||||
|
//
|
||||||
|
// By the time a status hits the manager interface, it should already have been filtered and it should be established that the status indeed
|
||||||
|
// belongs in the home timeline of the given account ID.
|
||||||
|
//
|
||||||
|
// The manager makes a distinction between *indexed* posts and *prepared* posts.
|
||||||
|
//
|
||||||
|
// Indexed posts consist of just that post's ID (in the database) and the time it was created. An indexed post takes up very little memory, so
|
||||||
|
// it's not a huge priority to keep trimming the indexed posts list.
|
||||||
|
//
|
||||||
|
// Prepared posts consist of the post's database ID, the time it was created, AND the apimodel representation of that post, for quick serialization.
|
||||||
|
// Prepared posts of course take up more memory than indexed posts, so they should be regularly pruned if they're not being actively served.
|
||||||
|
type Manager interface {
|
||||||
|
// Ingest takes one status and indexes it into the timeline for the given account ID.
|
||||||
|
//
|
||||||
|
// It should already be established before calling this function that the status/post actually belongs in the timeline!
|
||||||
|
Ingest(status *gtsmodel.Status, timelineAccountID string) error
|
||||||
|
// IngestAndPrepare takes one status and indexes it into the timeline for the given account ID, and then immediately prepares it for serving.
|
||||||
|
// This is useful in cases where we know the status will need to be shown at the top of a user's timeline immediately (eg., a new status is created).
|
||||||
|
//
|
||||||
|
// It should already be established before calling this function that the status/post actually belongs in the timeline!
|
||||||
|
IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error
|
||||||
|
// HomeTimeline returns limit n amount of entries from the home timeline of the given account ID, in descending chronological order.
|
||||||
|
// If maxID is provided, it will return entries from that maxID onwards, inclusive.
|
||||||
|
HomeTimeline(accountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error)
|
||||||
|
// GetIndexedLength returns the amount of posts/statuses that have been *indexed* for the given account ID.
|
||||||
|
GetIndexedLength(timelineAccountID string) int
|
||||||
|
// GetDesiredIndexLength returns the amount of posts that we, ideally, index for each user.
|
||||||
|
GetDesiredIndexLength() int
|
||||||
|
// GetOldestIndexedID returns the status ID for the oldest post that we have indexed for the given account.
|
||||||
|
GetOldestIndexedID(timelineAccountID string) (string, error)
|
||||||
|
// PrepareXFromTop prepares limit n amount of posts, based on their indexed representations, from the top of the index.
|
||||||
|
PrepareXFromTop(timelineAccountID string, limit int) error
|
||||||
|
// WipeStatusFromTimeline completely removes a status and from the index and prepared posts of the given account ID
|
||||||
|
//
|
||||||
|
// The returned int indicates how many entries were removed.
|
||||||
|
WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error)
|
||||||
|
// WipeStatusFromAllTimelines removes the status from the index and prepared posts of all timelines
|
||||||
|
WipeStatusFromAllTimelines(statusID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager returns a new timeline manager with the given database, typeconverter, config, and log.
|
||||||
|
func NewManager(db db.DB, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) Manager {
|
||||||
|
return &manager{
|
||||||
|
accountTimelines: sync.Map{},
|
||||||
|
db: db,
|
||||||
|
tc: tc,
|
||||||
|
config: config,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type manager struct {
|
||||||
|
accountTimelines sync.Map
|
||||||
|
db db.DB
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
config *config.Config
|
||||||
|
log *logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) Ingest(status *gtsmodel.Status, timelineAccountID string) error {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "Ingest",
|
||||||
|
"timelineAccountID": timelineAccountID,
|
||||||
|
"statusID": status.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
l.Trace("ingesting status")
|
||||||
|
return t.IndexOne(status.CreatedAt, status.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) IngestAndPrepare(status *gtsmodel.Status, timelineAccountID string) error {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "IngestAndPrepare",
|
||||||
|
"timelineAccountID": timelineAccountID,
|
||||||
|
"statusID": status.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
l.Trace("ingesting status")
|
||||||
|
return t.IndexAndPrepareOne(status.CreatedAt, status.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) Remove(statusID string, timelineAccountID string) (int, error) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "Remove",
|
||||||
|
"timelineAccountID": timelineAccountID,
|
||||||
|
"statusID": statusID,
|
||||||
|
})
|
||||||
|
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
l.Trace("removing status")
|
||||||
|
return t.Remove(statusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) HomeTimeline(timelineAccountID string, maxID string, sinceID string, minID string, limit int, local bool) ([]*apimodel.Status, error) {
|
||||||
|
l := m.log.WithFields(logrus.Fields{
|
||||||
|
"func": "HomeTimelineGet",
|
||||||
|
"timelineAccountID": timelineAccountID,
|
||||||
|
})
|
||||||
|
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
statuses, err := t.Get(limit, maxID, sinceID, minID)
|
||||||
|
if err != nil {
|
||||||
|
l.Errorf("error getting statuses: %s", err)
|
||||||
|
}
|
||||||
|
return statuses, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) GetIndexedLength(timelineAccountID string) int {
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
return t.PostIndexLength()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) GetDesiredIndexLength() int {
|
||||||
|
return desiredPostIndexLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) GetOldestIndexedID(timelineAccountID string) (string, error) {
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
return t.OldestIndexedPostID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) PrepareXFromTop(timelineAccountID string, limit int) error {
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
return t.PrepareFromTop(limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) WipeStatusFromTimeline(timelineAccountID string, statusID string) (int, error) {
|
||||||
|
t := m.getOrCreateTimeline(timelineAccountID)
|
||||||
|
|
||||||
|
return t.Remove(statusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) WipeStatusFromAllTimelines(statusID string) error {
|
||||||
|
errors := []string{}
|
||||||
|
m.accountTimelines.Range(func(k interface{}, i interface{}) bool {
|
||||||
|
t, ok := i.(Timeline)
|
||||||
|
if !ok {
|
||||||
|
panic("couldn't parse entry as Timeline, this should never happen so panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := t.Remove(statusID); err != nil {
|
||||||
|
errors = append(errors, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if len(errors) > 0 {
|
||||||
|
err = fmt.Errorf("one or more errors removing status %s from all timelines: %s", statusID, strings.Join(errors, ";"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *manager) getOrCreateTimeline(timelineAccountID string) Timeline {
|
||||||
|
var t Timeline
|
||||||
|
i, ok := m.accountTimelines.Load(timelineAccountID)
|
||||||
|
if !ok {
|
||||||
|
t = NewTimeline(timelineAccountID, m.db, m.tc, m.log)
|
||||||
|
m.accountTimelines.Store(timelineAccountID, t)
|
||||||
|
} else {
|
||||||
|
t, ok = i.(Timeline)
|
||||||
|
if !ok {
|
||||||
|
panic("couldn't parse entry as Timeline, this should never happen so panic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
57
internal/timeline/postindex.go
Normal file
57
internal/timeline/postindex.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type postIndex struct {
|
||||||
|
data *list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type postIndexEntry struct {
|
||||||
|
statusID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *postIndex) insertIndexed(i *postIndexEntry) error {
|
||||||
|
if p.data == nil {
|
||||||
|
p.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no entries yet, this is both the newest and oldest entry, so just put it in the front
|
||||||
|
if p.data.Len() == 0 {
|
||||||
|
p.data.PushFront(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var insertMark *list.Element
|
||||||
|
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
||||||
|
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
||||||
|
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("index: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the post to index is newer than e, insert it before e in the list
|
||||||
|
if insertMark == nil {
|
||||||
|
if i.statusID > entry.statusID {
|
||||||
|
insertMark = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we don't insert a duplicate
|
||||||
|
if entry.statusID == i.statusID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if insertMark != nil {
|
||||||
|
p.data.InsertBefore(i, insertMark)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we reach this point it's the oldest post we've seen so put it at the back
|
||||||
|
p.data.PushBack(i)
|
||||||
|
return nil
|
||||||
|
}
|
215
internal/timeline/prepare.go
Normal file
215
internal/timeline/prepare.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *timeline) prepareNextQuery(amount int, maxID string, sinceID string, minID string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// maxID is defined but sinceID isn't so take from behind
|
||||||
|
if maxID != "" && sinceID == "" {
|
||||||
|
err = t.PrepareBehind(maxID, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxID isn't defined, but sinceID || minID are, so take x before
|
||||||
|
if maxID == "" && sinceID != "" {
|
||||||
|
err = t.PrepareBefore(sinceID, false, amount)
|
||||||
|
}
|
||||||
|
if maxID == "" && minID != "" {
|
||||||
|
err = t.PrepareBefore(minID, false, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) PrepareBehind(statusID string, amount int) error {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
var prepared int
|
||||||
|
var preparing bool
|
||||||
|
prepareloop:
|
||||||
|
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("PrepareBehind: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !preparing {
|
||||||
|
// we haven't hit the position we need to prepare from yet
|
||||||
|
if entry.statusID == statusID {
|
||||||
|
preparing = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preparing {
|
||||||
|
if err := t.prepare(entry.statusID); err != nil {
|
||||||
|
// there's been an error
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
// it's a real error
|
||||||
|
return fmt.Errorf("PrepareBehind: error preparing status with id %s: %s", entry.statusID, err)
|
||||||
|
}
|
||||||
|
// the status just doesn't exist (anymore) so continue to the next one
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prepared == amount {
|
||||||
|
// we're done
|
||||||
|
break prepareloop
|
||||||
|
}
|
||||||
|
prepared = prepared + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) PrepareBefore(statusID string, include bool, amount int) error {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
var prepared int
|
||||||
|
var preparing bool
|
||||||
|
prepareloop:
|
||||||
|
for e := t.postIndex.data.Back(); e != nil; e = e.Prev() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("PrepareBefore: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !preparing {
|
||||||
|
// we haven't hit the position we need to prepare from yet
|
||||||
|
if entry.statusID == statusID {
|
||||||
|
preparing = true
|
||||||
|
if !include {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if preparing {
|
||||||
|
if err := t.prepare(entry.statusID); err != nil {
|
||||||
|
// there's been an error
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
// it's a real error
|
||||||
|
return fmt.Errorf("PrepareBefore: error preparing status with id %s: %s", entry.statusID, err)
|
||||||
|
}
|
||||||
|
// the status just doesn't exist (anymore) so continue to the next one
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prepared == amount {
|
||||||
|
// we're done
|
||||||
|
break prepareloop
|
||||||
|
}
|
||||||
|
prepared = prepared + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) PrepareFromTop(amount int) error {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
t.preparedPosts.data.Init()
|
||||||
|
|
||||||
|
var prepared int
|
||||||
|
prepareloop:
|
||||||
|
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("PrepareFromTop: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.prepare(entry.statusID); err != nil {
|
||||||
|
// there's been an error
|
||||||
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
|
// it's a real error
|
||||||
|
return fmt.Errorf("PrepareFromTop: error preparing status with id %s: %s", entry.statusID, err)
|
||||||
|
}
|
||||||
|
// the status just doesn't exist (anymore) so continue to the next one
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
prepared = prepared + 1
|
||||||
|
if prepared == amount {
|
||||||
|
// we're done
|
||||||
|
break prepareloop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) prepare(statusID string) error {
|
||||||
|
|
||||||
|
// start by getting the status out of the database according to its indexed ID
|
||||||
|
gtsStatus := >smodel.Status{}
|
||||||
|
if err := t.db.GetByID(statusID, gtsStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the account pointer hasn't been set on this timeline already, set it lazily here
|
||||||
|
if t.account == nil {
|
||||||
|
timelineOwnerAccount := >smodel.Account{}
|
||||||
|
if err := t.db.GetByID(t.accountID, timelineOwnerAccount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.account = timelineOwnerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
// to convert the status we need relevant accounts from it, so pull them out here
|
||||||
|
relevantAccounts, err := t.db.PullRelevantAccountsFromStatus(gtsStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if this is a boost...
|
||||||
|
var reblogOfStatus *gtsmodel.Status
|
||||||
|
if gtsStatus.BoostOfID != "" {
|
||||||
|
s := >smodel.Status{}
|
||||||
|
if err := t.db.GetByID(gtsStatus.BoostOfID, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reblogOfStatus = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// serialize the status (or, at least, convert it to a form that's ready to be serialized)
|
||||||
|
apiModelStatus, err := t.tc.StatusToMasto(gtsStatus, relevantAccounts.StatusAuthor, t.account, relevantAccounts.BoostedAccount, relevantAccounts.ReplyToAccount, reblogOfStatus)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// shove it in prepared posts as a prepared posts entry
|
||||||
|
preparedPostsEntry := &preparedPostsEntry{
|
||||||
|
statusID: statusID,
|
||||||
|
prepared: apiModelStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.preparedPosts.insertPrepared(preparedPostsEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) OldestPreparedPostID() (string, error) {
|
||||||
|
var id string
|
||||||
|
if t.preparedPosts == nil || t.preparedPosts.data == nil {
|
||||||
|
// return an empty string if prepared posts hasn't been initialized yet
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
e := t.preparedPosts.data.Back()
|
||||||
|
if e == nil {
|
||||||
|
// return an empty string if there's no back entry (ie., the index list hasn't been initialized yet)
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return id, errors.New("OldestPreparedPostID: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
return entry.statusID, nil
|
||||||
|
}
|
60
internal/timeline/preparedposts.go
Normal file
60
internal/timeline/preparedposts.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type preparedPosts struct {
|
||||||
|
data *list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type preparedPostsEntry struct {
|
||||||
|
statusID string
|
||||||
|
prepared *apimodel.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *preparedPosts) insertPrepared(i *preparedPostsEntry) error {
|
||||||
|
if p.data == nil {
|
||||||
|
p.data = &list.List{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we have no entries yet, this is both the newest and oldest entry, so just put it in the front
|
||||||
|
if p.data.Len() == 0 {
|
||||||
|
p.data.PushFront(i)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var insertMark *list.Element
|
||||||
|
// We need to iterate through the index to make sure we put this post in the appropriate place according to when it was created.
|
||||||
|
// We also need to make sure we're not inserting a duplicate post -- this can happen sometimes and it's not nice UX (*shudder*).
|
||||||
|
for e := p.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("index: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the post to index is newer than e, insert it before e in the list
|
||||||
|
if insertMark == nil {
|
||||||
|
if i.statusID > entry.statusID {
|
||||||
|
insertMark = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure we don't insert a duplicate
|
||||||
|
if entry.statusID == i.statusID {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if insertMark != nil {
|
||||||
|
p.data.InsertBefore(i, insertMark)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we reach this point it's the oldest post we've seen so put it at the back
|
||||||
|
p.data.PushBack(i)
|
||||||
|
return nil
|
||||||
|
}
|
50
internal/timeline/remove.go
Normal file
50
internal/timeline/remove.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t *timeline) Remove(statusID string) (int, error) {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
var removed int
|
||||||
|
|
||||||
|
// remove entr(ies) from the post index
|
||||||
|
removeIndexes := []*list.Element{}
|
||||||
|
if t.postIndex != nil && t.postIndex.data != nil {
|
||||||
|
for e := t.postIndex.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*postIndexEntry)
|
||||||
|
if !ok {
|
||||||
|
return removed, errors.New("Remove: could not parse e as a postIndexEntry")
|
||||||
|
}
|
||||||
|
if entry.statusID == statusID {
|
||||||
|
removeIndexes = append(removeIndexes, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range removeIndexes {
|
||||||
|
t.postIndex.data.Remove(e)
|
||||||
|
removed = removed + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove entr(ies) from prepared posts
|
||||||
|
removePrepared := []*list.Element{}
|
||||||
|
if t.preparedPosts != nil && t.preparedPosts.data != nil {
|
||||||
|
for e := t.preparedPosts.data.Front(); e != nil; e = e.Next() {
|
||||||
|
entry, ok := e.Value.(*preparedPostsEntry)
|
||||||
|
if !ok {
|
||||||
|
return removed, errors.New("Remove: could not parse e as a preparedPostsEntry")
|
||||||
|
}
|
||||||
|
if entry.statusID == statusID {
|
||||||
|
removePrepared = append(removePrepared, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range removePrepared {
|
||||||
|
t.preparedPosts.data.Remove(e)
|
||||||
|
removed = removed + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed, nil
|
||||||
|
}
|
139
internal/timeline/timeline.go
Normal file
139
internal/timeline/timeline.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
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 timeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Timeline represents a timeline for one account, and contains indexed and prepared posts.
|
||||||
|
type Timeline interface {
|
||||||
|
/*
|
||||||
|
RETRIEVAL FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
Get(amount int, maxID string, sinceID string, minID string) ([]*apimodel.Status, error)
|
||||||
|
// GetXFromTop returns x amount of posts from the top of the timeline, from newest to oldest.
|
||||||
|
GetXFromTop(amount int) ([]*apimodel.Status, error)
|
||||||
|
// GetXBehindID returns x amount of posts from the given id onwards, from newest to oldest.
|
||||||
|
// This will NOT include the status with the given ID.
|
||||||
|
//
|
||||||
|
// This corresponds to an api call to /timelines/home?max_id=WHATEVER
|
||||||
|
GetXBehindID(amount int, fromID string) ([]*apimodel.Status, error)
|
||||||
|
// GetXBeforeID returns x amount of posts up to the given id, from newest to oldest.
|
||||||
|
// This will NOT include the status with the given ID.
|
||||||
|
//
|
||||||
|
// This corresponds to an api call to /timelines/home?since_id=WHATEVER
|
||||||
|
GetXBeforeID(amount int, sinceID string, startFromTop bool) ([]*apimodel.Status, error)
|
||||||
|
// GetXBetweenID returns x amount of posts from the given maxID, up to the given id, from newest to oldest.
|
||||||
|
// This will NOT include the status with the given IDs.
|
||||||
|
//
|
||||||
|
// This corresponds to an api call to /timelines/home?since_id=WHATEVER&max_id=WHATEVER_ELSE
|
||||||
|
GetXBetweenID(amount int, maxID string, sinceID string) ([]*apimodel.Status, error)
|
||||||
|
|
||||||
|
/*
|
||||||
|
INDEXING FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property.
|
||||||
|
IndexOne(statusCreatedAt time.Time, statusID string) error
|
||||||
|
|
||||||
|
// OldestIndexedPostID returns the id of the rearmost (ie., the oldest) indexed post, or an error if something goes wrong.
|
||||||
|
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
||||||
|
OldestIndexedPostID() (string, error)
|
||||||
|
|
||||||
|
/*
|
||||||
|
PREPARATION FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// PrepareXFromTop instructs the timeline to prepare x amount of posts from the top of the timeline.
|
||||||
|
PrepareFromTop(amount int) error
|
||||||
|
// PrepareBehind instructs the timeline to prepare the next amount of entries for serialization, from position onwards.
|
||||||
|
// If include is true, then the given status ID will also be prepared, otherwise only entries behind it will be prepared.
|
||||||
|
PrepareBehind(statusID string, amount int) error
|
||||||
|
// IndexOne puts a status into the timeline at the appropriate place according to its 'createdAt' property,
|
||||||
|
// and then immediately prepares it.
|
||||||
|
IndexAndPrepareOne(statusCreatedAt time.Time, statusID string) error
|
||||||
|
// OldestPreparedPostID returns the id of the rearmost (ie., the oldest) prepared post, or an error if something goes wrong.
|
||||||
|
// If nothing goes wrong but there's no oldest post, an empty string will be returned so make sure to check for this.
|
||||||
|
OldestPreparedPostID() (string, error)
|
||||||
|
|
||||||
|
/*
|
||||||
|
INFO FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ActualPostIndexLength returns the actual length of the post index at this point in time.
|
||||||
|
PostIndexLength() int
|
||||||
|
|
||||||
|
/*
|
||||||
|
UTILITY FUNCTIONS
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Reset instructs the timeline to reset to its base state -- cache only the minimum amount of posts.
|
||||||
|
Reset() error
|
||||||
|
// Remove removes a status from both the index and prepared posts.
|
||||||
|
//
|
||||||
|
// If a status has multiple entries in a timeline, they will all be removed.
|
||||||
|
//
|
||||||
|
// The returned int indicates the amount of entries that were removed.
|
||||||
|
Remove(statusID string) (int, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeline fulfils the Timeline interface
|
||||||
|
type timeline struct {
|
||||||
|
postIndex *postIndex
|
||||||
|
preparedPosts *preparedPosts
|
||||||
|
accountID string
|
||||||
|
account *gtsmodel.Account
|
||||||
|
db db.DB
|
||||||
|
tc typeutils.TypeConverter
|
||||||
|
log *logrus.Logger
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimeline returns a new Timeline for the given account ID
|
||||||
|
func NewTimeline(accountID string, db db.DB, typeConverter typeutils.TypeConverter, log *logrus.Logger) Timeline {
|
||||||
|
return &timeline{
|
||||||
|
postIndex: &postIndex{},
|
||||||
|
preparedPosts: &preparedPosts{},
|
||||||
|
accountID: accountID,
|
||||||
|
db: db,
|
||||||
|
tc: typeConverter,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) Reset() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *timeline) PostIndexLength() int {
|
||||||
|
if t.postIndex == nil || t.postIndex.data == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.postIndex.data.Len()
|
||||||
|
}
|
|
@ -117,10 +117,13 @@ func (c *converter) ASRepresentationToAccount(accountable Accountable, update bo
|
||||||
|
|
||||||
// url property
|
// url property
|
||||||
url, err := extractURL(accountable)
|
url, err := extractURL(accountable)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return nil, fmt.Errorf("could not extract url for person with id %s: %s", uri.String(), err)
|
// take the URL if we can find it
|
||||||
|
acct.URL = url.String()
|
||||||
|
} else {
|
||||||
|
// otherwise just take the account URI as the URL
|
||||||
|
acct.URL = uri.String()
|
||||||
}
|
}
|
||||||
acct.URL = url.String()
|
|
||||||
|
|
||||||
// InboxURI
|
// InboxURI
|
||||||
if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil {
|
if accountable.GetActivityStreamsInbox() != nil && accountable.GetActivityStreamsInbox().GetIRI() != nil {
|
||||||
|
@ -222,7 +225,7 @@ func (c *converter) ASStatusToStatus(statusable Statusable) (*gtsmodel.Status, e
|
||||||
status.APStatusOwnerURI = attributedTo.String()
|
status.APStatusOwnerURI = attributedTo.String()
|
||||||
|
|
||||||
statusOwner := >smodel.Account{}
|
statusOwner := >smodel.Account{}
|
||||||
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String()}}, statusOwner); err != nil {
|
if err := c.db.GetWhere([]db.Where{{Key: "uri", Value: attributedTo.String(), CaseInsensitive: true}}, statusOwner); err != nil {
|
||||||
return nil, fmt.Errorf("couldn't get status owner from db: %s", err)
|
return nil, fmt.Errorf("couldn't get status owner from db: %s", err)
|
||||||
}
|
}
|
||||||
status.AccountID = statusOwner.ID
|
status.AccountID = statusOwner.ID
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +25,10 @@ func (c *converter) FollowRequestToFollow(f *gtsmodel.FollowRequest) *gtsmodel.F
|
||||||
func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.Account) (*gtsmodel.Status, error) {
|
func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.Account) (*gtsmodel.Status, error) {
|
||||||
// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs
|
// the wrapper won't use the same ID as the boosted status so we generate some new UUIDs
|
||||||
uris := util.GenerateURIsForAccount(boostingAccount.Username, c.config.Protocol, c.config.Host)
|
uris := util.GenerateURIsForAccount(boostingAccount.Username, c.config.Protocol, c.config.Host)
|
||||||
boostWrapperStatusID := uuid.NewString()
|
boostWrapperStatusID, err := id.NewULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID)
|
boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID)
|
||||||
boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID)
|
boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID)
|
||||||
|
|
||||||
|
@ -56,7 +59,7 @@ func (c *converter) StatusToBoost(s *gtsmodel.Status, boostingAccount *gtsmodel.
|
||||||
Emojis: []string{},
|
Emojis: []string{},
|
||||||
|
|
||||||
// the below fields will be taken from the target status
|
// the below fields will be taken from the target status
|
||||||
Content: util.HTMLFormat(s.Content),
|
Content: s.Content,
|
||||||
ContentWarning: s.ContentWarning,
|
ContentWarning: s.ContentWarning,
|
||||||
ActivityStreamsType: s.ActivityStreamsType,
|
ActivityStreamsType: s.ActivityStreamsType,
|
||||||
Sensitive: s.Sensitive,
|
Sensitive: s.Sensitive,
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account
|
||||||
func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, error) {
|
func (c *converter) AccountToMastoPublic(a *gtsmodel.Account) (*model.Account, error) {
|
||||||
// count followers
|
// count followers
|
||||||
followers := []gtsmodel.Follow{}
|
followers := []gtsmodel.Follow{}
|
||||||
if err := c.db.GetFollowersByAccountID(a.ID, &followers); err != nil {
|
if err := c.db.GetFollowersByAccountID(a.ID, &followers, false); err != nil {
|
||||||
if _, ok := err.(db.ErrNoEntries); !ok {
|
if _, ok := err.(db.ErrNoEntries); !ok {
|
||||||
return nil, fmt.Errorf("error getting followers: %s", err)
|
return nil, fmt.Errorf("error getting followers: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,7 +25,13 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi
|
||||||
update.SetActivityStreamsActor(actorProp)
|
update.SetActivityStreamsActor(actorProp)
|
||||||
|
|
||||||
// set the ID
|
// set the ID
|
||||||
idString := util.GenerateURIForUpdate(originAccount.Username, c.config.Protocol, c.config.Host, uuid.NewString())
|
|
||||||
|
newID, err := id.NewRandomULID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idString := util.GenerateURIForUpdate(originAccount.Username, c.config.Protocol, c.config.Host, newID)
|
||||||
idURI, err := url.Parse(idString)
|
idURI, err := url.Parse(idString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", idString, err)
|
return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", idString, err)
|
||||||
|
|
|
@ -41,11 +41,11 @@
|
||||||
mentionNameRegex = regexp.MustCompile(mentionNameRegexString)
|
mentionNameRegex = regexp.MustCompile(mentionNameRegexString)
|
||||||
|
|
||||||
// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1
|
// mention regex can be played around with here: https://regex101.com/r/qwM9D3/1
|
||||||
mentionFinderRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?: |\n)`
|
mentionFinderRegexString = `(?: |^|\W)(@[a-zA-Z0-9_]+(?:@[a-zA-Z0-9_\-\.]+)?)(?:[^a-zA-Z0-9]|\W|$)?`
|
||||||
mentionFinderRegex = regexp.MustCompile(mentionFinderRegexString)
|
mentionFinderRegex = regexp.MustCompile(mentionFinderRegexString)
|
||||||
|
|
||||||
// hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1
|
// hashtag regex can be played with here: https://regex101.com/r/Vhy8pg/1
|
||||||
hashtagFinderRegexString = fmt.Sprintf(`(?: |^|\W)?#([a-zA-Z0-9]{1,%d})(?:\b|\r)`, maximumHashtagLength)
|
hashtagFinderRegexString = fmt.Sprintf(`(?:\b)?#(\w{1,%d})(?:\b)`, maximumHashtagLength)
|
||||||
hashtagFinderRegex = regexp.MustCompile(hashtagFinderRegexString)
|
hashtagFinderRegex = regexp.MustCompile(hashtagFinderRegexString)
|
||||||
|
|
||||||
// emoji shortcode regex can be played with here: https://regex101.com/r/zMDRaG/1
|
// emoji shortcode regex can be played with here: https://regex101.com/r/zMDRaG/1
|
||||||
|
@ -85,21 +85,25 @@
|
||||||
// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following
|
// followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following
|
||||||
followingPathRegex = regexp.MustCompile(followingPathRegexString)
|
followingPathRegex = regexp.MustCompile(followingPathRegexString)
|
||||||
|
|
||||||
// see https://ihateregex.io/expr/uuid/
|
followPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, FollowPath, ulidRegexString)
|
||||||
uuidRegexString = `[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}`
|
// followPathRegex parses a path that validates and captures the username part and the ulid part
|
||||||
|
// from eg /users/example_username/follow/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
|
followPathRegex = regexp.MustCompile(followPathRegexString)
|
||||||
|
|
||||||
|
ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}`
|
||||||
|
|
||||||
likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath)
|
likedPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s$`, UsersPath, usernameRegexString, LikedPath)
|
||||||
// likedPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked
|
// likedPathRegex parses a path that validates and captures the username part from eg /users/example_username/liked
|
||||||
likedPathRegex = regexp.MustCompile(likedPathRegexString)
|
likedPathRegex = regexp.MustCompile(likedPathRegexString)
|
||||||
|
|
||||||
likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, uuidRegexString)
|
likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, ulidRegexString)
|
||||||
// likePathRegex parses a path that validates and captures the username part and the uuid part
|
// likePathRegex parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/liked/123e4567-e89b-12d3-a456-426655440000.
|
// from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
likePathRegex = regexp.MustCompile(likePathRegexString)
|
likePathRegex = regexp.MustCompile(likePathRegexString)
|
||||||
|
|
||||||
statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, uuidRegexString)
|
statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, ulidRegexString)
|
||||||
// statusesPathRegex parses a path that validates and captures the username part and the uuid part
|
// statusesPathRegex parses a path that validates and captures the username part and the ulid part
|
||||||
// from eg /users/example_username/statuses/123e4567-e89b-12d3-a456-426655440000.
|
// from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH
|
||||||
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
// The regex can be played with here: https://regex101.com/r/G9zuxQ/1
|
||||||
statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
|
statusesPathRegex = regexp.MustCompile(statusesPathRegexString)
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,7 +35,7 @@ func DeriveMentionsFromStatus(status string) []string {
|
||||||
for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) {
|
for _, m := range mentionFinderRegex.FindAllStringSubmatch(status, -1) {
|
||||||
mentionedAccounts = append(mentionedAccounts, m[1])
|
mentionedAccounts = append(mentionedAccounts, m[1])
|
||||||
}
|
}
|
||||||
return lower(unique(mentionedAccounts))
|
return unique(mentionedAccounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status,
|
// DeriveHashtagsFromStatus takes a plaintext (ie., not html-formatted) status,
|
||||||
|
@ -47,7 +47,7 @@ func DeriveHashtagsFromStatus(status string) []string {
|
||||||
for _, m := range hashtagFinderRegex.FindAllStringSubmatch(status, -1) {
|
for _, m := range hashtagFinderRegex.FindAllStringSubmatch(status, -1) {
|
||||||
tags = append(tags, m[1])
|
tags = append(tags, m[1])
|
||||||
}
|
}
|
||||||
return lower(unique(tags))
|
return unique(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status,
|
// DeriveEmojisFromStatus takes a plaintext (ie., not html-formatted) status,
|
||||||
|
@ -59,7 +59,7 @@ func DeriveEmojisFromStatus(status string) []string {
|
||||||
for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) {
|
for _, m := range emojiFinderRegex.FindAllStringSubmatch(status, -1) {
|
||||||
emojis = append(emojis, m[1])
|
emojis = append(emojis, m[1])
|
||||||
}
|
}
|
||||||
return lower(unique(emojis))
|
return unique(emojis)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractMentionParts extracts the username test_user and the domain example.org
|
// ExtractMentionParts extracts the username test_user and the domain example.org
|
||||||
|
@ -94,24 +94,3 @@ func unique(s []string) []string {
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
// lower lowercases all strings in a given string slice
|
|
||||||
func lower(s []string) []string {
|
|
||||||
new := []string{}
|
|
||||||
for _, i := range s {
|
|
||||||
new = append(new, strings.ToLower(i))
|
|
||||||
}
|
|
||||||
return new
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTMLFormat takes a plaintext formatted status string, and converts it into
|
|
||||||
// a nice HTML-formatted string.
|
|
||||||
//
|
|
||||||
// This includes:
|
|
||||||
// - Replacing line-breaks with <p>
|
|
||||||
// - Replacing URLs with hrefs.
|
|
||||||
// - Replacing mentions with links to that account's URL as stored in the database.
|
|
||||||
func HTMLFormat(status string) string {
|
|
||||||
// TODO: write proper HTML formatting logic for a status
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -108,19 +107,19 @@ type UserURIs struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateURIForFollow returns the AP URI for a new follow -- something like:
|
// GenerateURIForFollow returns the AP URI for a new follow -- something like:
|
||||||
// https://example.org/users/whatever_user/follow/41c7f33f-1060-48d9-84df-38dcb13cf0d8
|
// https://example.org/users/whatever_user/follow/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||||
func GenerateURIForFollow(username string, protocol string, host string, thisFollowID string) string {
|
func GenerateURIForFollow(username string, protocol string, host string, thisFollowID string) string {
|
||||||
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, FollowPath, thisFollowID)
|
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, FollowPath, thisFollowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateURIForLike returns the AP URI for a new like/fave -- something like:
|
// GenerateURIForLike returns the AP URI for a new like/fave -- something like:
|
||||||
// https://example.org/users/whatever_user/liked/41c7f33f-1060-48d9-84df-38dcb13cf0d8
|
// https://example.org/users/whatever_user/liked/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||||
func GenerateURIForLike(username string, protocol string, host string, thisFavedID string) string {
|
func GenerateURIForLike(username string, protocol string, host string, thisFavedID string) string {
|
||||||
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, LikedPath, thisFavedID)
|
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, LikedPath, thisFavedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateURIForUpdate returns the AP URI for a new update activity -- something like:
|
// GenerateURIForUpdate returns the AP URI for a new update activity -- something like:
|
||||||
// https://example.org/users/whatever_user#updates/41c7f33f-1060-48d9-84df-38dcb13cf0d8
|
// https://example.org/users/whatever_user#updates/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||||
func GenerateURIForUpdate(username string, protocol string, host string, thisUpdateID string) string {
|
func GenerateURIForUpdate(username string, protocol string, host string, thisUpdateID string) string {
|
||||||
return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)
|
return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID)
|
||||||
}
|
}
|
||||||
|
@ -162,58 +161,63 @@ func GenerateURIsForAccount(username string, protocol string, host string) *User
|
||||||
|
|
||||||
// IsUserPath returns true if the given URL path corresponds to eg /users/example_username
|
// IsUserPath returns true if the given URL path corresponds to eg /users/example_username
|
||||||
func IsUserPath(id *url.URL) bool {
|
func IsUserPath(id *url.URL) bool {
|
||||||
return userPathRegex.MatchString(strings.ToLower(id.Path))
|
return userPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox
|
// IsInboxPath returns true if the given URL path corresponds to eg /users/example_username/inbox
|
||||||
func IsInboxPath(id *url.URL) bool {
|
func IsInboxPath(id *url.URL) bool {
|
||||||
return inboxPathRegex.MatchString(strings.ToLower(id.Path))
|
return inboxPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsOutboxPath returns true if the given URL path corresponds to eg /users/example_username/outbox
|
// IsOutboxPath returns true if the given URL path corresponds to eg /users/example_username/outbox
|
||||||
func IsOutboxPath(id *url.URL) bool {
|
func IsOutboxPath(id *url.URL) bool {
|
||||||
return outboxPathRegex.MatchString(strings.ToLower(id.Path))
|
return outboxPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInstanceActorPath returns true if the given URL path corresponds to eg /actors/example_username
|
// IsInstanceActorPath returns true if the given URL path corresponds to eg /actors/example_username
|
||||||
func IsInstanceActorPath(id *url.URL) bool {
|
func IsInstanceActorPath(id *url.URL) bool {
|
||||||
return actorPathRegex.MatchString(strings.ToLower(id.Path))
|
return actorPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
|
// IsFollowersPath returns true if the given URL path corresponds to eg /users/example_username/followers
|
||||||
func IsFollowersPath(id *url.URL) bool {
|
func IsFollowersPath(id *url.URL) bool {
|
||||||
return followersPathRegex.MatchString(strings.ToLower(id.Path))
|
return followersPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsFollowingPath returns true if the given URL path corresponds to eg /users/example_username/following
|
// IsFollowingPath returns true if the given URL path corresponds to eg /users/example_username/following
|
||||||
func IsFollowingPath(id *url.URL) bool {
|
func IsFollowingPath(id *url.URL) bool {
|
||||||
return followingPathRegex.MatchString(strings.ToLower(id.Path))
|
return followingPathRegex.MatchString(id.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFollowPath returns true if the given URL path corresponds to eg /users/example_username/follow/SOME_ULID_OF_A_FOLLOW
|
||||||
|
func IsFollowPath(id *url.URL) bool {
|
||||||
|
return followPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked
|
// IsLikedPath returns true if the given URL path corresponds to eg /users/example_username/liked
|
||||||
func IsLikedPath(id *url.URL) bool {
|
func IsLikedPath(id *url.URL) bool {
|
||||||
return likedPathRegex.MatchString(strings.ToLower(id.Path))
|
return likedPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLikePath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_UUID_OF_A_STATUS
|
// IsLikePath returns true if the given URL path corresponds to eg /users/example_username/liked/SOME_ULID_OF_A_STATUS
|
||||||
func IsLikePath(id *url.URL) bool {
|
func IsLikePath(id *url.URL) bool {
|
||||||
return likePathRegex.MatchString(strings.ToLower(id.Path))
|
return likePathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_UUID_OF_A_STATUS
|
// IsStatusesPath returns true if the given URL path corresponds to eg /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
||||||
func IsStatusesPath(id *url.URL) bool {
|
func IsStatusesPath(id *url.URL) bool {
|
||||||
return statusesPathRegex.MatchString(strings.ToLower(id.Path))
|
return statusesPathRegex.MatchString(id.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseStatusesPath returns the username and uuid from a path such as /users/example_username/statuses/SOME_UUID_OF_A_STATUS
|
// ParseStatusesPath returns the username and ulid from a path such as /users/example_username/statuses/SOME_ULID_OF_A_STATUS
|
||||||
func ParseStatusesPath(id *url.URL) (username string, uuid string, err error) {
|
func ParseStatusesPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
matches := statusesPathRegex.FindStringSubmatch(id.Path)
|
||||||
if len(matches) != 3 {
|
if len(matches) != 3 {
|
||||||
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username = matches[1]
|
username = matches[1]
|
||||||
uuid = matches[2]
|
ulid = matches[2]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,14 +276,14 @@ func ParseFollowingPath(id *url.URL) (username string, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseLikedPath returns the username and uuid from a path such as /users/example_username/liked/SOME_UUID_OF_A_STATUS
|
// ParseLikedPath returns the username and ulid from a path such as /users/example_username/liked/SOME_ULID_OF_A_STATUS
|
||||||
func ParseLikedPath(id *url.URL) (username string, uuid string, err error) {
|
func ParseLikedPath(id *url.URL) (username string, ulid string, err error) {
|
||||||
matches := likePathRegex.FindStringSubmatch(id.Path)
|
matches := likePathRegex.FindStringSubmatch(id.Path)
|
||||||
if len(matches) != 3 {
|
if len(matches) != 3 {
|
||||||
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username = matches[1]
|
username = matches[1]
|
||||||
uuid = matches[2]
|
ulid = matches[2]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,5 +27,5 @@
|
||||||
|
|
||||||
// NewTestProcessor returns a Processor suitable for testing purposes
|
// NewTestProcessor returns a Processor suitable for testing purposes
|
||||||
func NewTestProcessor(db db.DB, storage blob.Storage, federator federation.Federator) processing.Processor {
|
func NewTestProcessor(db db.DB, storage blob.Storage, federator federation.Federator) processing.Processor {
|
||||||
return processing.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog())
|
return processing.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, NewTestTimelineManager(db), db, NewTestLog())
|
||||||
}
|
}
|
||||||
|
|
11
testrig/timelinemanager.go
Normal file
11
testrig/timelinemanager.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package testrig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTestTimelineManager retuts a new timeline.Manager, suitable for testing, using the given db.
|
||||||
|
func NewTestTimelineManager(db db.DB) timeline.Manager {
|
||||||
|
return timeline.NewManager(db, NewTestTypeConverter(db), NewTestConfig(), NewTestLog())
|
||||||
|
}
|
Loading…
Reference in a new issue