From 26ee190338a50598ec4126139bfa21494e73ef26 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Fri, 11 Jun 2021 18:38:58 +0200 Subject: [PATCH] move to ulid --- go.mod | 1 + go.sum | 2 + .../api/client/account/accountcreate_test.go | 369 ------------------ internal/api/client/auth/signin.go | 2 +- internal/api/client/fileserver/fileserver.go | 2 +- internal/api/model/timeline.go | 2 +- internal/cliactions/server/server.go | 28 +- internal/db/pg/pg.go | 35 +- internal/federation/federatingdb/create.go | 21 + internal/federation/federatingdb/util.go | 18 +- internal/federation/federatingprotocol.go | 7 + internal/gtsmodel/account.go | 10 +- internal/gtsmodel/application.go | 4 +- internal/gtsmodel/block.go | 6 +- internal/gtsmodel/domainblock.go | 4 +- internal/gtsmodel/emaildomainblock.go | 4 +- internal/gtsmodel/emoji.go | 4 +- internal/gtsmodel/follow.go | 6 +- internal/gtsmodel/followrequest.go | 6 +- internal/gtsmodel/instance.go | 6 +- internal/gtsmodel/mediaattachment.go | 8 +- internal/gtsmodel/mention.go | 8 +- internal/gtsmodel/notification.go | 8 +- internal/gtsmodel/status.go | 12 +- internal/gtsmodel/statusbookmark.go | 8 +- internal/gtsmodel/statusfave.go | 8 +- internal/gtsmodel/statusmute.go | 8 +- internal/gtsmodel/tag.go | 4 +- internal/gtsmodel/user.go | 8 +- internal/id/ulid.go | 51 +++ internal/media/handler.go | 10 +- internal/media/processicon.go | 9 +- internal/media/processimage.go | 9 +- internal/oauth/clientstore.go | 2 +- internal/oauth/tokenstore.go | 15 +- internal/processing/account.go | 8 +- internal/processing/admin.go | 7 + internal/processing/app.go | 12 +- internal/processing/federation.go | 8 +- internal/processing/fromcommon.go | 31 ++ internal/processing/fromfederator.go | 32 +- internal/processing/search.go | 20 +- .../processing/synchronous/status/context.go | 2 +- .../processing/synchronous/status/create.go | 7 +- .../processing/synchronous/status/fave.go | 7 +- .../processing/synchronous/status/util.go | 7 + internal/processing/timeline.go | 12 +- internal/typeutils/internal.go | 7 +- internal/typeutils/wrap.go | 10 +- internal/util/regexes.go | 15 +- internal/util/uri.go | 22 +- 51 files changed, 398 insertions(+), 514 deletions(-) create mode 100644 internal/id/ulid.go diff --git a/go.mod b/go.mod index 8a5139b..aec00d3 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect 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/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 47b798a..37dadf6 100644 --- a/go.sum +++ b/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.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 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.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= diff --git a/internal/api/client/account/accountcreate_test.go b/internal/api/client/account/accountcreate_test.go index da86ee9..6757763 100644 --- a/internal/api/client/account/accountcreate_test.go +++ b/internal/api/client/account/accountcreate_test.go @@ -17,372 +17,3 @@ // */ 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)) -// } diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go index e9385e3..158cc5c 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -74,7 +74,7 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) { // ValidatePassword takes an email address and a password. // 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. func (m *Module) ValidatePassword(email string, password string) (userid string, err error) { l := m.log.WithField("func", "ValidatePassword") diff --git a/internal/api/client/fileserver/fileserver.go b/internal/api/client/fileserver/fileserver.go index b06f480..08e6abb 100644 --- a/internal/api/client/fileserver/fileserver.go +++ b/internal/api/client/fileserver/fileserver.go @@ -32,7 +32,7 @@ import ( ) 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" // MediaTypeKey is the url key for media type (usually something like attachment or header etc) MediaTypeKey = "media_type" diff --git a/internal/api/model/timeline.go b/internal/api/model/timeline.go index f4f7880..6949813 100644 --- a/internal/api/model/timeline.go +++ b/internal/api/model/timeline.go @@ -1,6 +1,6 @@ package model type StatusTimelineResponse struct { - Statuses []*Status + Statuses []*Status LinkHeader string } diff --git a/internal/cliactions/server/server.go b/internal/cliactions/server/server.go index 1f0ceb3..f055890 100644 --- a/internal/cliactions/server/server.go +++ b/internal/cliactions/server/server.go @@ -75,6 +75,20 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log 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) router, err := router.New(c, log) @@ -151,20 +165,6 @@ var Start cliactions.GTSAction = func(ctx context.Context, c *config.Config, log } } - 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) if err != nil { return fmt.Errorf("error creating gotosocial service: %s", err) diff --git a/internal/db/pg/pg.go b/internal/db/pg/pg.go index 44657aa..5e92c1a 100644 --- a/internal/db/pg/pg.go +++ b/internal/db/pg/pg.go @@ -33,11 +33,11 @@ import ( "github.com/go-pg/pg/extra/pgdebug" "github.com/go-pg/pg/v10" "github.com/go-pg/pg/v10/orm" - "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/util" "golang.org/x/crypto/bcrypt" ) @@ -334,6 +334,7 @@ func (ps *postgresService) AcceptFollowRequest(originAccountID string, targetAcc // create a new follow to 'replace' the request with follow := >smodel.Follow{ + ID: fr.ID, AccountID: originAccountID, TargetAccountID: targetAccountID, URI: fr.URI, @@ -360,8 +361,14 @@ func (ps *postgresService) CreateInstanceAccount() error { return err } + aID, err := id.NewRandomULID() + if err != nil { + return err + } + newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) a := >smodel.Account{ + ID: aID, Username: ps.config.Host, DisplayName: username, URL: newAccountURIs.UserURL, @@ -389,7 +396,13 @@ func (ps *postgresService) CreateInstanceAccount() error { } func (ps *postgresService) CreateInstanceInstance() error { + iID, err := id.NewRandomULID() + if err != nil { + return err + } + i := >smodel.Instance{ + ID: iID, Domain: ps.config.Host, Title: ps.config.Host, URI: fmt.Sprintf("%s://%s", ps.config.Protocol, ps.config.Host), @@ -600,8 +613,13 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr } newAccountURIs := util.GenerateURIsForAccount(username, ps.config.Protocol, ps.config.Host) + newAccountID, err := id.NewRandomULID() + if err != nil { + return nil, err + } a := >smodel.Account{ + ID: newAccountID, Username: username, DisplayName: username, Reason: reason, @@ -625,8 +643,15 @@ func (ps *postgresService) NewSignup(username string, reason string, requireAppr if err != nil { return nil, fmt.Errorf("error hashing password: %s", err) } + + newUserID, err := id.NewRandomULID() + if err != nil { + return nil, err + } + u := >smodel.User{ - AccountID: a.ID, + ID: newUserID, + AccountID: newAccountID, EncryptedPassword: string(pw), SignUpIP: signUpIP, Locale: locale, @@ -1364,7 +1389,11 @@ func (ps *postgresService) TagStringsToTags(tags []string, originAccountID strin if err := ps.conn.Model(tag).Where("LOWER(?) = LOWER(?)", pg.Ident("name"), t).Select(); err != nil { if err == pg.ErrNoRows { // 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.FirstSeenFromAccountID = originAccountID diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index f707d44..04b0a7f 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -29,6 +29,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -99,6 +100,14 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error { if err != nil { 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 _, ok := err.(db.ErrAlreadyExists); ok { // the status already exists in the database, which means we've already handled everything else, @@ -128,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) } + newID, err := id.NewULIDFromTime(followRequest.CreatedAt) + if err != nil { + return err + } + followRequest.ID = newID + if err := f.db.Put(followRequest); err != nil { return fmt.Errorf("database error inserting follow request: %s", err) } @@ -149,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) } + newID, err := id.NewULIDFromTime(fave.CreatedAt) + if err != nil { + return err + } + fave.ID = newID + if err := f.db.Put(fave); err != nil { return fmt.Errorf("database error inserting fave: %s", err) } diff --git a/internal/federation/federatingdb/util.go b/internal/federation/federatingdb/util.go index ff6ae50..ed3c252 100644 --- a/internal/federation/federatingdb/util.go +++ b/internal/federation/federatingdb/util.go @@ -27,10 +27,10 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "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 // 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( logrus.Fields{ "func": "NewID", @@ -99,7 +99,11 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err if iter.IsIRI() { 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 - 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 - return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString())) + // fallback default behavior: just return a random ULID after our protocol and host + 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. diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index e05bdb7..8784c32 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -31,6 +31,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "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) } + aID, err := id.NewRandomULID() + if err != nil { + return ctx, false, err + } + a.ID = aID + if err := f.db.Put(a); err != nil { l.Errorf("error inserting dereferenced remote account: %s", err) } diff --git a/internal/gtsmodel/account.go b/internal/gtsmodel/account.go index 04eb58e..ba9963a 100644 --- a/internal/gtsmodel/account.go +++ b/internal/gtsmodel/account.go @@ -33,8 +33,8 @@ type Account struct { BASIC INFO */ - // id of this account 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 of this account in the local database + 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 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. @@ -45,11 +45,11 @@ type Account struct { */ // 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? AvatarRemoteURL string // 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? HeaderRemoteURL string // 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? Memorial bool // This account has moved this account id in the database - MovedToAccountID string + MovedToAccountID string `pg:"type:CHAR(26)"` // When was this account created? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this account last updated? diff --git a/internal/gtsmodel/application.go b/internal/gtsmodel/application.go index 8e1398b..91287df 100644 --- a/internal/gtsmodel/application.go +++ b/internal/gtsmodel/application.go @@ -22,7 +22,7 @@ package gtsmodel // It is used to authorize tokens etc, and is associated with an oauth client id in the database. type Application struct { // 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 string // 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 RedirectURI string // 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 ClientSecret string // scopes requested when this app was created diff --git a/internal/gtsmodel/block.go b/internal/gtsmodel/block.go index fae43fb..27b3972 100644 --- a/internal/gtsmodel/block.go +++ b/internal/gtsmodel/block.go @@ -5,15 +5,15 @@ import "time" // Block refers to the blocking of one account by another. type Block struct { // 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 CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Who created this block? - AccountID string `pg:",notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` // Who is targeted by this block? - TargetAccountID string `pg:",notnull"` + TargetAccountID string `pg:"type:CHAR(26),notnull"` // Activitypub URI for this block URI string } diff --git a/internal/gtsmodel/domainblock.go b/internal/gtsmodel/domainblock.go index dcfb2ac..f5c96d8 100644 --- a/internal/gtsmodel/domainblock.go +++ b/internal/gtsmodel/domainblock.go @@ -23,7 +23,7 @@ import "time" // DomainBlock represents a federation block against a particular domain, of varying severity. type DomainBlock struct { // 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. // For example: 'example.org' also blocks 'gts.example.org'. '.com' blocks *any* '.com' domains. // TODO: implement wildcards here @@ -33,7 +33,7 @@ type DomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:",notnull"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` // TODO: define this Severity int // Reject media from this domain? diff --git a/internal/gtsmodel/emaildomainblock.go b/internal/gtsmodel/emaildomainblock.go index 4cda68b..5155855 100644 --- a/internal/gtsmodel/emaildomainblock.go +++ b/internal/gtsmodel/emaildomainblock.go @@ -23,7 +23,7 @@ import "time" // EmailDomainBlock represents a domain that the server should automatically reject sign-up requests from. type EmailDomainBlock struct { // 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' Domain string `pg:",notnull"` // When was this block created @@ -31,5 +31,5 @@ type EmailDomainBlock struct { // When was this block updated UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // Account ID of the creator of this block - CreatedByAccountID string `pg:",notnull"` + CreatedByAccountID string `pg:"type:CHAR(26),notnull"` } diff --git a/internal/gtsmodel/emoji.go b/internal/gtsmodel/emoji.go index c175a1c..2fa3b75 100644 --- a/internal/gtsmodel/emoji.go +++ b/internal/gtsmodel/emoji.go @@ -23,7 +23,7 @@ import "time" // Emoji represents a custom emoji that's been uploaded through the admin UI, and is useable by instance denizens. type Emoji struct { // 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_ // eg., 'blob_hug' 'purple_heart' Must be unique with domain. Shortcode string `pg:",notnull,unique:shortcodedomain"` @@ -73,5 +73,5 @@ type Emoji struct { // Is this emoji visible in the admin emoji picker? VisibleInPicker bool `pg:",notnull,default:true"` // In which emoji category is this emoji visible? - CategoryID string + CategoryID string `pg:"type:CHAR(26)"` } diff --git a/internal/gtsmodel/follow.go b/internal/gtsmodel/follow.go index 90080da..f5a170c 100644 --- a/internal/gtsmodel/follow.go +++ b/internal/gtsmodel/follow.go @@ -23,15 +23,15 @@ import "time" // Follow represents one account following another, and the metadata around that follow. type Follow struct { // 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? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this follow last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // 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? - 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? ShowReblogs bool `pg:"default:true"` // What is the activitypub URI of this follow? diff --git a/internal/gtsmodel/followrequest.go b/internal/gtsmodel/followrequest.go index 1401a26..aabb785 100644 --- a/internal/gtsmodel/followrequest.go +++ b/internal/gtsmodel/followrequest.go @@ -23,15 +23,15 @@ import "time" // FollowRequest represents one account requesting to follow another, and the metadata around that request. type FollowRequest struct { // 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? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this follow request last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // 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? - 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? ShowReblogs bool `pg:"default:true"` // What is the activitypub URI of this follow request? diff --git a/internal/gtsmodel/instance.go b/internal/gtsmodel/instance.go index f6a6f4c..8b97ea2 100644 --- a/internal/gtsmodel/instance.go +++ b/internal/gtsmodel/instance.go @@ -5,7 +5,7 @@ import "time" // Instance represents a federated instance, either local or remote. type Instance struct { // 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 Domain string `pg:",notnull,unique"` // 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? SuspendedAt time.Time // 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 ShortDescription string // Longer description of this instance @@ -27,7 +27,7 @@ type Instance struct { // Contact email address for this instance ContactEmail string // Contact account ID in the database for this instance - ContactAccountID string + ContactAccountID string `pg:"type:CHAR(26)"` // Reputation score of this instance Reputation int64 `pg:",notnull,default:0"` // Version of the software used on this instance diff --git a/internal/gtsmodel/mediaattachment.go b/internal/gtsmodel/mediaattachment.go index e986028..2aeeee9 100644 --- a/internal/gtsmodel/mediaattachment.go +++ b/internal/gtsmodel/mediaattachment.go @@ -26,9 +26,9 @@ import ( // somewhere in storage and that can be retrieved and served by the router. type MediaAttachment struct { // 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 - StatusID string + StatusID string `pg:"type:CHAR(26)"` // Where can the attachment be retrieved on *this* server URL string // 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 FileMeta FileMeta // 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 string // To which scheduled status does this attachment belong - ScheduledStatusID string + ScheduledStatusID string `pg:"type:CHAR(26)"` // What is the generated blurhash of this attachment Blurhash string // What is the processing status of this attachment diff --git a/internal/gtsmodel/mention.go b/internal/gtsmodel/mention.go index 013e9cc..47c7805 100644 --- a/internal/gtsmodel/mention.go +++ b/internal/gtsmodel/mention.go @@ -23,19 +23,19 @@ import "time" // Mention refers to the 'tagging' or 'mention' of a user within a status. type Mention struct { // 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 - StatusID string `pg:",notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` // When was this mention created? CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // When was this mention last updated? UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // 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? OriginAccountURI string `pg:",notnull"` // 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? Silent bool diff --git a/internal/gtsmodel/notification.go b/internal/gtsmodel/notification.go index 5084d46..efd4fe4 100644 --- a/internal/gtsmodel/notification.go +++ b/internal/gtsmodel/notification.go @@ -23,17 +23,17 @@ import "time" // Notification models an alert/notification sent to an account about something like a reblog, like, new follow request, etc. type Notification struct { // 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 NotificationType NotificationType `pg:",notnull"` // Creation time of this notification CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // 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? - 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? - StatusID string + StatusID string `pg:"type:CHAR(26)"` // Has this notification been read already? Read bool diff --git a/internal/gtsmodel/status.go b/internal/gtsmodel/status.go index 6572b03..f5e3329 100644 --- a/internal/gtsmodel/status.go +++ b/internal/gtsmodel/status.go @@ -23,7 +23,7 @@ import "time" // Status represents a user-created 'post' or 'status' in the database, either remote or local type Status struct { // 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 string `pg:",unique"` // web url for viewing this status @@ -45,13 +45,13 @@ type Status struct { // is this status from a local account? Local bool // which account posted this status? - AccountID string + AccountID string `pg:"type:CHAR(26),notnull"` // 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 - InReplyToAccountID string + InReplyToAccountID string `pg:"type:CHAR(26)"` // id of the status this status is a boost of - BoostOfID string + BoostOfID string `pg:"type:CHAR(26)"` // cw string for this status ContentWarning string // visibility entry for this status @@ -61,7 +61,7 @@ type Status struct { // what language is this status written in? Language string // Which application was used to create this status? - CreatedWithApplicationID string + CreatedWithApplicationID string `pg:"type:CHAR(26)"` // advanced visibility for this status VisibilityAdvanced *VisibilityAdvanced // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types diff --git a/internal/gtsmodel/statusbookmark.go b/internal/gtsmodel/statusbookmark.go index 6246334..7d95067 100644 --- a/internal/gtsmodel/statusbookmark.go +++ b/internal/gtsmodel/statusbookmark.go @@ -23,13 +23,13 @@ import "time" // StatusBookmark refers to one account having a 'bookmark' of the status of another account type StatusBookmark struct { // id of this bookmark in the database - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + ID string `pg:"type:CHAR(26),pk,notnull,unique"` // when was this bookmark created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the bookmarking - AccountID string `pg:",notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` // 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 - StatusID string `pg:",notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` } diff --git a/internal/gtsmodel/statusfave.go b/internal/gtsmodel/statusfave.go index efbc37e..7152db3 100644 --- a/internal/gtsmodel/statusfave.go +++ b/internal/gtsmodel/statusfave.go @@ -23,15 +23,15 @@ import "time" // StatusFave refers to a 'fave' or 'like' in the database, from one account, targeting the status of another account type StatusFave struct { // id of this fave in the database - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + ID string `pg:"type:CHAR(26),pk,notnull,unique"` // when was this fave created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the fave - AccountID string `pg:",notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` // 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' - StatusID string `pg:",notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` // ActivityPub URI of this fave URI string `pg:",notnull"` diff --git a/internal/gtsmodel/statusmute.go b/internal/gtsmodel/statusmute.go index 53c15e5..6cd2b73 100644 --- a/internal/gtsmodel/statusmute.go +++ b/internal/gtsmodel/statusmute.go @@ -23,13 +23,13 @@ import "time" // StatusMute refers to one account having muted the status of another account or its own type StatusMute struct { // id of this mute in the database - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` + ID string `pg:"type:CHAR(26),pk,notnull,unique"` // when was this mute created CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // id of the account that created ('did') the mute - AccountID string `pg:",notnull"` + AccountID string `pg:"type:CHAR(26),notnull"` // 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 - StatusID string `pg:",notnull"` + StatusID string `pg:"type:CHAR(26),notnull"` } diff --git a/internal/gtsmodel/tag.go b/internal/gtsmodel/tag.go index c1b0429..c151e34 100644 --- a/internal/gtsmodel/tag.go +++ b/internal/gtsmodel/tag.go @@ -23,13 +23,13 @@ import "time" // Tag represents a hashtag for gathering public statuses together type Tag struct { // 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 URL string // name of this tag -- the tag without the hash part Name string `pg:",unique,pk,notnull"` // 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 CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` // when was this tag last updated diff --git a/internal/gtsmodel/user.go b/internal/gtsmodel/user.go index a725699..a1e912e 100644 --- a/internal/gtsmodel/user.go +++ b/internal/gtsmodel/user.go @@ -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 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 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) - 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 EncryptedPassword string `pg:",notnull"` @@ -60,7 +60,7 @@ type User struct { // How many times has this user signed in? SignInCount int // 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? ChosenLanguages []string // What languages does this user not want to see? @@ -68,7 +68,7 @@ type User struct { // In what timezone/locale is this user located? Locale string // Which application id created this user? See gtsmodel.Application - CreatedByApplicationID string + CreatedByApplicationID string `pg:"type:CHAR(26)"` // When did we last contact this user LastEmailedAt time.Time `pg:"type:timestamp"` diff --git a/internal/id/ulid.go b/internal/id/ulid.go new file mode 100644 index 0000000..b488ddf --- /dev/null +++ b/internal/id/ulid.go @@ -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 +} diff --git a/internal/media/handler.go b/internal/media/handler.go index acfc823..0bcf464 100644 --- a/internal/media/handler.go +++ b/internal/media/handler.go @@ -26,12 +26,12 @@ import ( "strings" "time" - "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/blob" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/transport" ) @@ -242,9 +242,11 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( // create the urls and storage paths 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, - // but we need it below so we should create it here instead. - newEmojiID := uuid.NewString() + // generate a id for the new emoji + newEmojiID, err := id.NewRandomULID() + if err != nil { + return nil, err + } // webfinger uri for the emoji -- unrelated to actually serving the image // will be something like https://example.org/emoji/70a7f3d7-7e35-4098-8ce3-9b5e8203bb9c diff --git a/internal/media/processicon.go b/internal/media/processicon.go index bc2c558..5fae631 100644 --- a/internal/media/processicon.go +++ b/internal/media/processicon.go @@ -24,8 +24,8 @@ import ( "strings" "time" - "github.com/google/uuid" "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) { @@ -72,9 +72,12 @@ func (mh *mediaHandler) processHeaderOrAvi(imageBytes []byte, contentType string 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] - 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) originalURL := fmt.Sprintf("%s/%s/%s/original/%s.%s", URLbase, accountID, mediaType, newMediaID, extension) diff --git a/internal/media/processimage.go b/internal/media/processimage.go index dd8bff0..d4add02 100644 --- a/internal/media/processimage.go +++ b/internal/media/processimage.go @@ -24,8 +24,8 @@ import ( "strings" "time" - "github.com/google/uuid" "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) { @@ -58,9 +58,12 @@ func (mh *mediaHandler) processImageAttachment(data []byte, accountID string, co 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] - 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) originalURL := fmt.Sprintf("%s/%s/attachment/original/%s.%s", URLbase, accountID, newMediaID, extension) diff --git a/internal/oauth/clientstore.go b/internal/oauth/clientstore.go index 5241cf4..998f678 100644 --- a/internal/oauth/clientstore.go +++ b/internal/oauth/clientstore.go @@ -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 type Client struct { - ID string + ID string `pg:"type:CHAR(26),pk,notnull"` Secret string Domain string UserID string diff --git a/internal/oauth/tokenstore.go b/internal/oauth/tokenstore.go index 04319ee..5f8e078 100644 --- a/internal/oauth/tokenstore.go +++ b/internal/oauth/tokenstore.go @@ -26,6 +26,7 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/models" ) @@ -98,7 +99,17 @@ func (pts *tokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error if !ok { 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 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 // and pgTokenToOauthToken can be used for that. type Token struct { - ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` + ID string `pg:"type:CHAR(26),pk,notnull"` ClientID string UserID string RedirectURI string diff --git a/internal/processing/account.go b/internal/processing/account.go index c570001..8707341 100644 --- a/internal/processing/account.go +++ b/internal/processing/account.go @@ -22,11 +22,11 @@ import ( "errors" "fmt" - "github.com/google/uuid" 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/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -426,8 +426,10 @@ func (p *processor) AccountFollowCreate(authed *oauth.Auth, form *apimodel.Accou } // make the follow request - - newFollowID := uuid.NewString() + newFollowID, err := id.NewRandomULID() + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } fr := >smodel.FollowRequest{ ID: newFollowID, diff --git a/internal/processing/admin.go b/internal/processing/admin.go index 78979a2..6ee3a05 100644 --- a/internal/processing/admin.go +++ b/internal/processing/admin.go @@ -25,6 +25,7 @@ import ( "io" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/id" "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) } + emojiID, err := id.NewULID() + if err != nil { + return nil, err + } + emoji.ID = emojiID + mastoEmoji, err := p.tc.EmojiToMasto(emoji) if err != nil { return nil, fmt.Errorf("error converting emoji to mastotype: %s", err) diff --git a/internal/processing/app.go b/internal/processing/app.go index 47fce05..7da5344 100644 --- a/internal/processing/app.go +++ b/internal/processing/app.go @@ -22,6 +22,7 @@ import ( "github.com/google/uuid" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "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 - clientID := uuid.NewString() + clientID, err := id.NewRandomULID() + if err != nil { + return nil, err + } clientSecret := uuid.NewString() vapidKey := uuid.NewString() + appID, err := id.NewRandomULID() + if err != nil { + return nil, err + } + // generate the application to put in the database app := >smodel.Application{ + ID: appID, Name: form.ClientName, Website: form.Website, RedirectURI: form.RedirectURIs, diff --git a/internal/processing/federation.go b/internal/processing/federation.go index eaa27d1..1c0d67f 100644 --- a/internal/processing/federation.go +++ b/internal/processing/federation.go @@ -29,6 +29,7 @@ import ( "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" ) @@ -74,7 +75,12 @@ 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) } - // 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 { return nil, fmt.Errorf("database error inserting account with uri %s: %s", requestingAccountURI.String(), err) } diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index ce4b17a..85531d2 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -25,6 +25,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" ) func (p *processor) notifyStatus(status *gtsmodel.Status) error { @@ -79,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 + notifID, err := id.NewULID() + if err != nil { + return err + } + notif := >smodel.Notification{ + ID: notifID, NotificationType: gtsmodel.NotificationMention, TargetAccountID: m.TargetAccountID, OriginAccountID: status.AccountID, @@ -100,7 +107,13 @@ func (p *processor) notifyFollowRequest(followRequest *gtsmodel.FollowRequest, r return nil } + notifID, err := id.NewULID() + if err != nil { + return err + } + notif := >smodel.Notification{ + ID: notifID, NotificationType: gtsmodel.NotificationFollowRequest, TargetAccountID: followRequest.TargetAccountID, OriginAccountID: followRequest.AccountID, @@ -129,7 +142,13 @@ func (p *processor) notifyFollow(follow *gtsmodel.Follow, receivingAccount *gtsm } // now create the new follow notification + notifID, err := id.NewULID() + if err != nil { + return err + } + notif := >smodel.Notification{ + ID: notifID, NotificationType: gtsmodel.NotificationFollow, TargetAccountID: follow.TargetAccountID, OriginAccountID: follow.AccountID, @@ -147,7 +166,13 @@ func (p *processor) notifyFave(fave *gtsmodel.StatusFave, receivingAccount *gtsm return nil } + notifID, err := id.NewULID() + if err != nil { + return err + } + notif := >smodel.Notification{ + ID: notifID, NotificationType: gtsmodel.NotificationFave, TargetAccountID: fave.TargetAccountID, OriginAccountID: fave.AccountID, @@ -200,7 +225,13 @@ func (p *processor) notifyAnnounce(status *gtsmodel.Status) error { } // now create the new reblog notification + notifID, err := id.NewULID() + if err != nil { + return err + } + notif := >smodel.Notification{ + ID: notifID, NotificationType: gtsmodel.NotificationReblog, TargetAccountID: boostedAcct.ID, OriginAccountID: status.AccountID, diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 0bd141a..2a44ac1 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -23,10 +23,10 @@ import ( "fmt" "net/url" - "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" ) func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) error { @@ -109,6 +109,12 @@ func (p *processor) processFromFederator(federatorMsg gtsmodel.FromFederator) er 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 _, ok := err.(db.ErrAlreadyExists); !ok { return fmt.Errorf("error adding dereferenced announce to the db: %s", err) @@ -212,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 // because we'll need it further down if status.ID == "" { - status.ID = uuid.NewString() + newID, err := id.NewULIDFromTime(status.CreatedAt) + if err != nil { + return err + } + status.ID = newID } // 1. Media attachments. @@ -364,12 +374,12 @@ func (p *processor) dereferenceAnnounce(announce *gtsmodel.Status, requestingUse } // 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 { 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 { return fmt.Errorf("dereferenceAnnounce: error dereferencing remote status with id %s: %s", announce.GTSBoostedStatus.URI, err) } @@ -397,7 +407,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) } - // 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 { return fmt.Errorf("dereferenceAnnounce: error putting dereferenced account with id %s into database : %s", accountURI.String(), err) } @@ -413,7 +428,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) } - // 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 { return fmt.Errorf("dereferenceAnnounce: error putting dereferenced status with id %s into the db: %s", announce.GTSBoostedStatus.URI, err) } diff --git a/internal/processing/search.go b/internal/processing/search.go index 4b9282b..d518a03 100644 --- a/internal/processing/search.go +++ b/internal/processing/search.go @@ -29,6 +29,7 @@ import ( "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/oauth" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -168,7 +169,12 @@ func (p *processor) searchStatusByURI(authed *oauth.Auth, uri *url.URL, resolve 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 { return nil, fmt.Errorf("error putting status in the db: %s", err) } @@ -211,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) } + accountID, err := id.NewRandomULID() + if err != nil { + return nil, err + } + account.ID = accountID + if err := p.db.Put(account); err != nil { return nil, fmt.Errorf("searchAccountByURI: error inserting account with uri %s: %s", uri.String(), err) } @@ -281,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) } + foundAccountID, err := id.NewULID() + if err != nil { + return nil, err + } + foundAccount.ID = foundAccountID + // put this new account in our database if err := p.db.Put(foundAccount); err != nil { return nil, fmt.Errorf("searchAccountByMention: error inserting account with uri %s: %s", acctURI.String(), err) diff --git a/internal/processing/synchronous/status/context.go b/internal/processing/synchronous/status/context.go index 8848315..cac8681 100644 --- a/internal/processing/synchronous/status/context.go +++ b/internal/processing/synchronous/status/context.go @@ -8,7 +8,7 @@ import ( func (p *processor) Context(account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { return &apimodel.Context{ - Ancestors: []apimodel.Status{}, + Ancestors: []apimodel.Status{}, Descendants: []apimodel.Status{}, }, nil } diff --git a/internal/processing/synchronous/status/create.go b/internal/processing/synchronous/status/create.go index 2b5e0cf..07f670d 100644 --- a/internal/processing/synchronous/status/create.go +++ b/internal/processing/synchronous/status/create.go @@ -4,16 +4,19 @@ import ( "fmt" "time" - "github.com/google/uuid" 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 := uuid.NewString() + 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) diff --git a/internal/processing/synchronous/status/fave.go b/internal/processing/synchronous/status/fave.go index ea06240..b4622ab 100644 --- a/internal/processing/synchronous/status/fave.go +++ b/internal/processing/synchronous/status/fave.go @@ -4,11 +4,11 @@ import ( "errors" "fmt" - "github.com/google/uuid" 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" ) @@ -66,7 +66,10 @@ func (p *processor) Fave(account *gtsmodel.Account, targetStatusID string) (*api } if newFave { - thisFaveID := uuid.NewString() + 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{ diff --git a/internal/processing/synchronous/status/util.go b/internal/processing/synchronous/status/util.go index 582dd4b..0a023ea 100644 --- a/internal/processing/synchronous/status/util.go +++ b/internal/processing/synchronous/status/util.go @@ -8,6 +8,7 @@ import ( 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" ) @@ -183,6 +184,12 @@ func (p *processor) processMentions(form *apimodel.AdvancedStatusCreateForm, acc 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) } diff --git a/internal/processing/timeline.go b/internal/processing/timeline.go index 204d7d8..ec49ecd 100644 --- a/internal/processing/timeline.go +++ b/internal/processing/timeline.go @@ -35,12 +35,12 @@ import ( func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.StatusTimelineResponse, gtserror.WithCode) { l := p.log.WithFields(logrus.Fields{ - "func": "HomeTimelineGet", - "maxID": maxID, + "func": "HomeTimelineGet", + "maxID": maxID, "sinceID": sinceID, - "minID": minID, - "limit": limit, - "local": local, + "minID": minID, + "limit": limit, + "local": local, }) resp := &apimodel.StatusTimelineResponse{ @@ -53,7 +53,7 @@ func (p *processor) HomeTimelineGet(authed *oauth.Auth, maxID string, sinceID st sinceIDMarker := sinceID minIDMarker := minID -l.Debugf("\n entering grabloop \n") + l.Debugf("\n entering grabloop \n") grabloop: for len(apiStatuses) < limit { l.Debugf("\n querying the db \n") diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go index 2342f5f..3b3c8bd 100644 --- a/internal/typeutils/internal.go +++ b/internal/typeutils/internal.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "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) { // 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) - boostWrapperStatusID := uuid.NewString() + boostWrapperStatusID, err := id.NewULID() + if err != nil { + return nil, err + } boostWrapperStatusURI := fmt.Sprintf("%s/%s", uris.StatusesURI, boostWrapperStatusID) boostWrapperStatusURL := fmt.Sprintf("%s/%s", uris.StatusesURL, boostWrapperStatusID) diff --git a/internal/typeutils/wrap.go b/internal/typeutils/wrap.go index fde6fda..e06da25 100644 --- a/internal/typeutils/wrap.go +++ b/internal/typeutils/wrap.go @@ -6,8 +6,8 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -25,7 +25,13 @@ func (c *converter) WrapPersonInUpdate(person vocab.ActivityStreamsPerson, origi update.SetActivityStreamsActor(actorProp) // 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) if err != nil { return nil, fmt.Errorf("WrapPersonInUpdate: error parsing url %s: %s", idString, err) diff --git a/internal/util/regexes.go b/internal/util/regexes.go index 1dcef25..8c61f59 100644 --- a/internal/util/regexes.go +++ b/internal/util/regexes.go @@ -85,21 +85,20 @@ var ( // followingPathRegex parses a path that validates and captures the username part from eg /users/example_username/following followingPathRegex = regexp.MustCompile(followingPathRegexString) - // see https://ihateregex.io/expr/uuid/ - 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}` + ulidRegexString = `[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}` 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 = regexp.MustCompile(likedPathRegexString) - likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, uuidRegexString) - // likePathRegex parses a path that validates and captures the username part and the uuid part - // from eg /users/example_username/liked/123e4567-e89b-12d3-a456-426655440000. + likePathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, LikedPath, ulidRegexString) + // likePathRegex parses a path that validates and captures the username part and the ulid part + // from eg /users/example_username/like/01F7XT5JZW1WMVSW1KADS8PVDH likePathRegex = regexp.MustCompile(likePathRegexString) - statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, uuidRegexString) - // statusesPathRegex parses a path that validates and captures the username part and the uuid part - // from eg /users/example_username/statuses/123e4567-e89b-12d3-a456-426655440000. + statusesPathRegexString = fmt.Sprintf(`^/?%s/(%s)/%s/(%s)$`, UsersPath, usernameRegexString, StatusesPath, ulidRegexString) + // statusesPathRegex parses a path that validates and captures the username part and the ulid part + // from eg /users/example_username/statuses/01F7XT5JZW1WMVSW1KADS8PVDH // The regex can be played with here: https://regex101.com/r/G9zuxQ/1 statusesPathRegex = regexp.MustCompile(statusesPathRegexString) ) diff --git a/internal/util/uri.go b/internal/util/uri.go index 86a39a7..f86f224 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -108,19 +108,19 @@ type UserURIs struct { } // 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 { 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: -// 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 { 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: -// 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 { return fmt.Sprintf("%s://%s/%s/%s#%s/%s", protocol, host, UsersPath, username, UpdatePath, thisUpdateID) } @@ -195,25 +195,25 @@ func IsLikedPath(id *url.URL) bool { return likedPathRegex.MatchString(strings.ToLower(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 { return likePathRegex.MatchString(strings.ToLower(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 { return statusesPathRegex.MatchString(strings.ToLower(id.Path)) } -// ParseStatusesPath returns the username and uuid from a path such as /users/example_username/statuses/SOME_UUID_OF_A_STATUS -func ParseStatusesPath(id *url.URL) (username string, uuid string, err error) { +// 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, ulid string, err error) { matches := statusesPathRegex.FindStringSubmatch(id.Path) if len(matches) != 3 { err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches)) return } username = matches[1] - uuid = matches[2] + ulid = matches[2] return } @@ -272,14 +272,14 @@ func ParseFollowingPath(id *url.URL) (username string, err error) { return } -// ParseLikedPath returns the username and uuid from a path such as /users/example_username/liked/SOME_UUID_OF_A_STATUS -func ParseLikedPath(id *url.URL) (username string, uuid string, err error) { +// 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, ulid string, err error) { matches := likePathRegex.FindStringSubmatch(id.Path) if len(matches) != 3 { err = fmt.Errorf("expected 3 matches but matches length was %d", len(matches)) return } username = matches[1] - uuid = matches[2] + ulid = matches[2] return }