move to ulid

This commit is contained in:
tsmethurst 2021-06-11 18:38:58 +02:00
parent 625e6d654c
commit 26ee190338
51 changed files with 398 additions and 514 deletions

1
go.mod
View File

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

2
go.sum
View File

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

View File

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

View File

@ -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")

View File

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

View File

@ -1,6 +1,6 @@
package model
type StatusTimelineResponse struct {
Statuses []*Status
Statuses []*Status
LinkHeader string
}

View File

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

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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

View File

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

View File

@ -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 := &gtsmodel.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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)"`
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"`

View File

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

View File

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

View File

@ -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"`

51
internal/id/ulid.go Normal file
View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 := &gtsmodel.FollowRequest{
ID: newFollowID,

View File

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

View File

@ -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 := &gtsmodel.Application{
ID: appID,
Name: form.ClientName,
Website: form.Website,
RedirectURI: form.RedirectURIs,

View File

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

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.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 := &gtsmodel.Notification{
ID: notifID,
NotificationType: gtsmodel.NotificationReblog,
TargetAccountID: boostedAcct.ID,
OriginAccountID: status.AccountID,

View File

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

View File

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

View File

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

View File

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

View File

@ -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 := &gtsmodel.StatusFave{

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

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