diff --git a/api/application_test.go b/api/application_test.go index 9e6efe74..03f81571 100644 --- a/api/application_test.go +++ b/api/application_test.go @@ -611,16 +611,16 @@ func upload(values map[string]*os.File) (contentType string, buffer bytes.Buffer for key, r := range values { var fw io.Writer if fw, err = w.CreateFormFile(key, r.Name()); err != nil { - return + return contentType, buffer, err } if _, err = io.Copy(fw, r); err != nil { - return + return contentType, buffer, err } } contentType = w.FormDataContentType() w.Close() - return + return contentType, buffer, err } func mustOpen(f string) *os.File { diff --git a/api/user.go b/api/user.go index bfaf88be..02d9aab3 100644 --- a/api/user.go +++ b/api/user.go @@ -19,7 +19,7 @@ type UserDatabase interface { DeleteUserByID(id uint) error UpdateUser(user *model.User) error CreateUser(user *model.User) error - CountUser(condition ...interface{}) (int, error) + CountUser(condition ...interface{}) (int64, error) } // UserChangeNotifier notifies listeners for user changes. diff --git a/auth/authentication_test.go b/auth/authentication_test.go index 9d3a626b..92efff43 100644 --- a/auth/authentication_test.go +++ b/auth/authentication_test.go @@ -84,7 +84,7 @@ func (s *AuthenticationSuite) assertQueryRequest(key, value string, f fMiddlewar ctx.Request = httptest.NewRequest("GET", fmt.Sprintf("/?%s=%s", key, value), nil) f()(ctx) assert.Equal(s.T(), code, recorder.Code) - return + return ctx } func (s *AuthenticationSuite) TestNothingProvided() { @@ -217,7 +217,7 @@ func (s *AuthenticationSuite) assertHeaderRequest(key, value string, f fMiddlewa ctx.Request.Header.Set(key, value) f()(ctx) assert.Equal(s.T(), code, recorder.Code) - return + return ctx } type fMiddleware func() gin.HandlerFunc diff --git a/database/application.go b/database/application.go index f62f4688..7f5ace59 100644 --- a/database/application.go +++ b/database/application.go @@ -4,7 +4,7 @@ import ( "time" "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" + "gorm.io/gorm" ) // GetApplicationByToken returns the application for the given token or nil. diff --git a/database/client.go b/database/client.go index 8ab5b3f8..f85c4948 100644 --- a/database/client.go +++ b/database/client.go @@ -4,7 +4,7 @@ import ( "time" "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" + "gorm.io/gorm" ) // GetClientByID returns the client for the given id or nil. diff --git a/database/database.go b/database/database.go index 7920c8c6..fd4bd2f7 100644 --- a/database/database.go +++ b/database/database.go @@ -1,16 +1,21 @@ package database import ( + "errors" + "log" "os" "path/filepath" "time" "github.com/gotify/server/v2/auth/password" + "github.com/gotify/server/v2/mode" "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" - _ "github.com/jinzhu/gorm/dialects/mysql" // enable the mysql dialect. - _ "github.com/jinzhu/gorm/dialects/postgres" // enable the postgres dialect. - _ "github.com/jinzhu/gorm/dialects/sqlite" // enable the sqlite3 dialect. + "github.com/mattn/go-isatty" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" ) var mkdirAll = os.MkdirAll @@ -19,21 +24,53 @@ var mkdirAll = os.MkdirAll func New(dialect, connection, defaultUser, defaultPass string, strength int, createDefaultUserIfNotExist bool) (*GormDatabase, error) { createDirectoryIfSqlite(dialect, connection) - db, err := gorm.Open(dialect, connection) + logLevel := logger.Info + if mode.Get() == mode.Prod { + logLevel = logger.Warn + } + + dbLogger := logger.New(log.New(os.Stderr, "\r\n", log.LstdFlags), logger.Config{ + SlowThreshold: 200 * time.Millisecond, + LogLevel: logLevel, + IgnoreRecordNotFoundError: true, + Colorful: isatty.IsTerminal(os.Stderr.Fd()), + }) + gormConfig := &gorm.Config{ + Logger: dbLogger, + DisableForeignKeyConstraintWhenMigrating: true, + } + + var db *gorm.DB + err := errors.New("unsupported dialect: " + dialect) + + switch dialect { + case "mysql": + db, err = gorm.Open(mysql.Open(connection), gormConfig) + case "postgres": + db, err = gorm.Open(postgres.Open(connection), gormConfig) + case "sqlite3": + db, err = gorm.Open(sqlite.Open(connection), gormConfig) + } + + if err != nil { + return nil, err + } + + sqldb, err := db.DB() if err != nil { return nil, err } // We normally don't need that much connections, so we limit them. F.ex. mysql complains about // "too many connections", while load testing Gotify. - db.DB().SetMaxOpenConns(10) + sqldb.SetMaxOpenConns(10) if dialect == "sqlite3" { // We use the database connection inside the handlers from the http // framework, therefore concurrent access occurs. Sqlite cannot handle // concurrent writes, so we limit sqlite to one connection. // see https://github.com/mattn/go-sqlite3/issues/274 - db.DB().SetMaxOpenConns(1) + sqldb.SetMaxOpenConns(1) } if dialect == "mysql" { @@ -41,18 +78,14 @@ func New(dialect, connection, defaultUser, defaultPass string, strength int, cre // after which a connection may not be used anymore. // The default for this setting on mariadb is 10 minutes. // See https://github.com/docker-library/mariadb/issues/113 - db.DB().SetConnMaxLifetime(9 * time.Minute) + sqldb.SetConnMaxLifetime(9 * time.Minute) } - if err := db.AutoMigrate(new(model.User), new(model.Application), new(model.Message), new(model.Client), new(model.PluginConf)).Error; err != nil { + if err := db.AutoMigrate(new(model.User), new(model.Application), new(model.Message), new(model.Client), new(model.PluginConf)); err != nil { return nil, err } - if err := prepareBlobColumn(dialect, db); err != nil { - return nil, err - } - - userCount := 0 + userCount := int64(0) db.Find(new(model.User)).Count(&userCount) if createDefaultUserIfNotExist && userCount == 0 { db.Create(&model.User{Name: defaultUser, Pass: password.CreatePassword(defaultPass, strength), Admin: true}) @@ -61,31 +94,6 @@ func New(dialect, connection, defaultUser, defaultPass string, strength int, cre return &GormDatabase{DB: db}, nil } -func prepareBlobColumn(dialect string, db *gorm.DB) error { - blobType := "" - switch dialect { - case "mysql": - blobType = "longblob" - case "postgres": - blobType = "bytea" - } - if blobType != "" { - for _, target := range []struct { - Table interface{} - Column string - }{ - {model.Message{}, "extras"}, - {model.PluginConf{}, "config"}, - {model.PluginConf{}, "storage"}, - } { - if err := db.Model(target.Table).ModifyColumn(target.Column, blobType).Error; err != nil { - return err - } - } - } - return nil -} - func createDirectoryIfSqlite(dialect, connection string) { if dialect == "sqlite3" { if _, err := os.Stat(filepath.Dir(connection)); os.IsNotExist(err) { @@ -103,5 +111,9 @@ type GormDatabase struct { // Close closes the gorm database connection. func (d *GormDatabase) Close() { - d.DB.Close() + sqldb, err := d.DB.DB() + if err != nil { + return + } + sqldb.Close() } diff --git a/database/message.go b/database/message.go index be351440..b8b23175 100644 --- a/database/message.go +++ b/database/message.go @@ -2,7 +2,7 @@ package database import ( "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" + "gorm.io/gorm" ) // GetMessageByID returns the messages for the given id or nil. @@ -27,7 +27,7 @@ func (d *GormDatabase) CreateMessage(message *model.Message) error { func (d *GormDatabase) GetMessagesByUser(userID uint) ([]*model.Message, error) { var messages []*model.Message err := d.DB.Joins("JOIN applications ON applications.user_id = ?", userID). - Where("messages.application_id = applications.id").Order("id desc").Find(&messages).Error + Where("messages.application_id = applications.id").Order("messages.id desc").Find(&messages).Error if err == gorm.ErrRecordNotFound { err = nil } @@ -39,7 +39,7 @@ func (d *GormDatabase) GetMessagesByUser(userID uint) ([]*model.Message, error) func (d *GormDatabase) GetMessagesByUserSince(userID uint, limit int, since uint) ([]*model.Message, error) { var messages []*model.Message db := d.DB.Joins("JOIN applications ON applications.user_id = ?", userID). - Where("messages.application_id = applications.id").Order("id desc").Limit(limit) + Where("messages.application_id = applications.id").Order("messages.id desc").Limit(limit) if since != 0 { db = db.Where("messages.id < ?", since) } @@ -53,7 +53,7 @@ func (d *GormDatabase) GetMessagesByUserSince(userID uint, limit int, since uint // GetMessagesByApplication returns all messages from an application. func (d *GormDatabase) GetMessagesByApplication(tokenID uint) ([]*model.Message, error) { var messages []*model.Message - err := d.DB.Where("application_id = ?", tokenID).Order("id desc").Find(&messages).Error + err := d.DB.Where("application_id = ?", tokenID).Order("messages.id desc").Find(&messages).Error if err == gorm.ErrRecordNotFound { err = nil } @@ -64,7 +64,7 @@ func (d *GormDatabase) GetMessagesByApplication(tokenID uint) ([]*model.Message, // If since is 0 it will be ignored. func (d *GormDatabase) GetMessagesByApplicationSince(appID uint, limit int, since uint) ([]*model.Message, error) { var messages []*model.Message - db := d.DB.Where("application_id = ?", appID).Order("id desc").Limit(limit) + db := d.DB.Where("application_id = ?", appID).Order("messages.id desc").Limit(limit) if since != 0 { db = db.Where("messages.id < ?", since) } diff --git a/database/migration_test.go b/database/migration_test.go index 4feb3752..dc3d10e4 100644 --- a/database/migration_test.go +++ b/database/migration_test.go @@ -5,9 +5,10 @@ import ( "github.com/gotify/server/v2/model" "github.com/gotify/server/v2/test" - "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "gorm.io/driver/sqlite" + "gorm.io/gorm" ) func TestMigration(t *testing.T) { @@ -21,18 +22,20 @@ type MigrationSuite struct { func (s *MigrationSuite) BeforeTest(suiteName, testName string) { s.tmpDir = test.NewTmpDir("gotify_migrationsuite") - db, err := gorm.Open("sqlite3", s.tmpDir.Path("test_obsolete.db")) - assert.Nil(s.T(), err) - defer db.Close() + db, err := gorm.Open(sqlite.Open(s.tmpDir.Path("test_obsolete.db")), &gorm.Config{}) + assert.NoError(s.T(), err) + sqlDB, err := db.DB() + assert.NoError(s.T(), err) + defer sqlDB.Close() - assert.Nil(s.T(), db.CreateTable(new(model.User)).Error) + assert.Nil(s.T(), db.Migrator().CreateTable(new(model.User))) assert.Nil(s.T(), db.Create(&model.User{ Name: "test_user", Admin: true, }).Error) // we should not be able to create applications by now - assert.False(s.T(), db.HasTable(new(model.Application))) + assert.False(s.T(), db.Migrator().HasTable(new(model.Application))) } func (s *MigrationSuite) AfterTest(suiteName, testName string) { @@ -44,7 +47,7 @@ func (s *MigrationSuite) TestMigration() { assert.Nil(s.T(), err) defer db.Close() - assert.True(s.T(), db.DB.HasTable(new(model.Application))) + assert.True(s.T(), db.DB.Migrator().HasTable(new(model.Application))) // a user already exist, not adding a new user if user, err := db.GetUserByName("admin"); assert.NoError(s.T(), err) { diff --git a/database/ping.go b/database/ping.go index 181bc4ad..4c46587e 100644 --- a/database/ping.go +++ b/database/ping.go @@ -2,5 +2,9 @@ package database // Ping pings the database to verify the connection. func (d *GormDatabase) Ping() error { - return d.DB.DB().Ping() + sqldb, err := d.DB.DB() + if err != nil { + return err + } + return sqldb.Ping() } diff --git a/database/plugin.go b/database/plugin.go index 2287fc19..98a0301e 100644 --- a/database/plugin.go +++ b/database/plugin.go @@ -2,7 +2,7 @@ package database import ( "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" + "gorm.io/gorm" ) // GetPluginConfByUser gets plugin configurations from a user. diff --git a/database/user.go b/database/user.go index 01f08ce5..8e6bab35 100644 --- a/database/user.go +++ b/database/user.go @@ -2,7 +2,7 @@ package database import ( "github.com/gotify/server/v2/model" - "github.com/jinzhu/gorm" + "gorm.io/gorm" ) // GetUserByName returns the user by the given name or nil. @@ -32,8 +32,8 @@ func (d *GormDatabase) GetUserByID(id uint) (*model.User, error) { } // CountUser returns the user count which satisfies the given condition. -func (d *GormDatabase) CountUser(condition ...interface{}) (int, error) { - c := -1 +func (d *GormDatabase) CountUser(condition ...interface{}) (int64, error) { + c := int64(-1) handle := d.DB.Model(new(model.User)) if len(condition) == 1 { handle = handle.Where(condition[0]) diff --git a/database/user_test.go b/database/user_test.go index af1cf245..78a5f737 100644 --- a/database/user_test.go +++ b/database/user_test.go @@ -21,7 +21,7 @@ func (s *DatabaseSuite) TestUser() { adminCount, err := s.db.CountUser("admin = ?", true) require.NoError(s.T(), err) - assert.Equal(s.T(), 1, adminCount, 1, "there is initially one admin") + assert.Equal(s.T(), int64(1), adminCount, "there is initially one admin") users, err := s.db.GetUsers() require.NoError(s.T(), err) @@ -33,7 +33,7 @@ func (s *DatabaseSuite) TestUser() { assert.NotEqual(s.T(), 0, nicories.ID, "on create user a new id should be assigned") userCount, err := s.db.CountUser() require.NoError(s.T(), err) - assert.Equal(s.T(), 2, userCount, "two users should exist") + assert.Equal(s.T(), int64(2), userCount, "two users should exist") user, err = s.db.GetUserByName("nicories") require.NoError(s.T(), err) @@ -60,7 +60,7 @@ func (s *DatabaseSuite) TestUser() { adminCount, err = s.db.CountUser(&model.User{Admin: true}) require.NoError(s.T(), err) - assert.Equal(s.T(), 2, adminCount, "two admins exist") + assert.Equal(s.T(), int64(2), adminCount, "two admins exist") require.NoError(s.T(), s.db.DeleteUserByID(tom.ID)) users, err = s.db.GetUsers() diff --git a/go.mod b/go.mod index a27d687b..24ba37eb 100644 --- a/go.mod +++ b/go.mod @@ -11,15 +11,20 @@ require ( github.com/gotify/plugin-api v1.0.0 github.com/h2non/filetype v1.1.3 github.com/jinzhu/configor v1.2.2 - github.com/jinzhu/gorm v1.9.16 + github.com/mattn/go-isatty v0.0.20 github.com/robfig/cron v1.2.0 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.43.0 gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/driver/postgres v1.6.0 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.0 ) require ( - github.com/BurntSushi/toml v1.2.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect @@ -29,17 +34,20 @@ require ( github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.5.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/lib/pq v1.10.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.7 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect diff --git a/go.sum b/go.sum index 1fe4a2e0..598ac7a2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= -github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= @@ -14,10 +16,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= @@ -40,14 +38,14 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -60,14 +58,22 @@ github.com/gotify/plugin-api v1.0.0 h1:kab40p2TEPLzjmcafOc7JOz75aTsYQyS2PXtElH8x github.com/gotify/plugin-api v1.0.0/go.mod h1:xZfEyqVK/Zvu3RwA/CtpuiwFmzFDxifrrqMaH9BHnyU= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA= github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8= -github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= -github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -79,15 +85,13 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.0 h1:Zx5DJFEYQXio93kgXnQ09fXNiUKsqv4OUEu2UtGcB1E= -github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= -github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA= -github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -110,6 +114,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -124,31 +129,20 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190109145017-48ac38b7c8cb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= @@ -164,3 +158,11 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/model/application.go b/model/application.go index 0a45f2e4..b08a56ca 100644 --- a/model/application.go +++ b/model/application.go @@ -13,13 +13,13 @@ type Application struct { // read only: true // required: true // example: 5 - ID uint `gorm:"primary_key;unique_index;AUTO_INCREMENT" json:"id"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` // The application token. Can be used as `appToken`. See Authentication. // // read only: true // required: true // example: AWH0wZ5r0Mbac.r - Token string `gorm:"type:varchar(180);unique_index" json:"token"` + Token string `gorm:"type:varchar(180);uniqueIndex:uix_applications_token" json:"token"` UserID uint `gorm:"index" json:"-"` // The application name. This is how the application should be displayed to the user. // @@ -43,7 +43,7 @@ type Application struct { // required: true // example: image/image.jpeg Image string `gorm:"type:text" json:"image"` - Messages []MessageExternal `json:"-"` + Messages []MessageExternal `gorm:"-" json:"-"` // The default priority of messages sent by this application. Defaults to 0. // // required: false diff --git a/model/client.go b/model/client.go index c858165e..9b96c82a 100644 --- a/model/client.go +++ b/model/client.go @@ -13,13 +13,13 @@ type Client struct { // read only: true // required: true // example: 5 - ID uint `gorm:"primary_key;unique_index;AUTO_INCREMENT" json:"id"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` // The client token. Can be used as `clientToken`. See Authentication. // // read only: true // required: true // example: CWH0wZ5r0Mbac.r - Token string `gorm:"type:varchar(180);unique_index" json:"token"` + Token string `gorm:"type:varchar(180);uniqueIndex:uix_clients_token" json:"token"` UserID uint `gorm:"index" json:"-"` // The client name. This is how the client should be displayed to the user. // diff --git a/model/message.go b/model/message.go index dbee2ec6..e00545a2 100644 --- a/model/message.go +++ b/model/message.go @@ -6,7 +6,7 @@ import ( // Message holds information about a message. type Message struct { - ID uint `gorm:"AUTO_INCREMENT;primary_key;index"` + ID uint `gorm:"autoIncrement;primaryKey;index"` ApplicationID uint Message string `gorm:"type:text"` Title string `gorm:"type:text"` diff --git a/model/pluginconf.go b/model/pluginconf.go index d7aed8f7..9f798817 100644 --- a/model/pluginconf.go +++ b/model/pluginconf.go @@ -2,10 +2,10 @@ package model // PluginConf holds information about the plugin. type PluginConf struct { - ID uint `gorm:"primary_key;AUTO_INCREMENT;index"` + ID uint `gorm:"primaryKey;autoIncrement"` UserID uint ModulePath string `gorm:"type:text"` - Token string `gorm:"type:varchar(180);unique_index"` + Token string `gorm:"type:varchar(180);uniqueIndex:uix_plugin_confs_token"` ApplicationID uint Enabled bool Config []byte diff --git a/model/user.go b/model/user.go index bde1056b..7593851e 100644 --- a/model/user.go +++ b/model/user.go @@ -2,8 +2,8 @@ package model // The User holds information about the credentials of a user and its application and client tokens. type User struct { - ID uint `gorm:"primary_key;unique_index;AUTO_INCREMENT"` - Name string `gorm:"type:varchar(180);unique_index"` + ID uint `gorm:"primaryKey;autoIncrement"` + Name string `gorm:"type:varchar(180);uniqueIndex:uix_users_name"` Pass []byte Admin bool Applications []Application diff --git a/plugin/compat/wrap_test_race.go b/plugin/compat/wrap_test_race.go index 0bf9c39a..b5668b90 100644 --- a/plugin/compat/wrap_test_race.go +++ b/plugin/compat/wrap_test_race.go @@ -1,3 +1,4 @@ +//go:build race // +build race package compat diff --git a/plugin/example/echo/echo.go b/plugin/example/echo/echo.go index 3def0602..b8b8407d 100644 --- a/plugin/example/echo/echo.go +++ b/plugin/example/echo/echo.go @@ -75,7 +75,6 @@ func (c *EchoPlugin) Disable() error { func (c *EchoPlugin) RegisterWebhook(baseURL string, g *gin.RouterGroup) { c.basePath = baseURL g.GET("/echo", func(ctx *gin.Context) { - storage, _ := c.storageHandler.Load() conf := new(Storage) json.Unmarshal(storage, conf) diff --git a/plugin/manager_test.go b/plugin/manager_test.go index 0a03cc68..8d698966 100644 --- a/plugin/manager_test.go +++ b/plugin/manager_test.go @@ -20,7 +20,6 @@ import ( "github.com/gotify/server/v2/plugin/testing/mock" "github.com/gotify/server/v2/test" "github.com/gotify/server/v2/test/testdb" - "github.com/jinzhu/gorm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -367,36 +366,6 @@ func TestNewManager_NonPluginFile_expectError(t *testing.T) { assert.Error(t, err) } -func TestNewManager_FaultyDB_expectError(t *testing.T) { - tmpDir := test.NewTmpDir("gotify_testnewmanager_faultydb") - defer tmpDir.Clean() - for _, data := range []struct { - pkg string - faultyTable string - name string - }{{"plugin/example/minimal/", "plugin_confs", "minimal"}, {"plugin/example/clock/", "applications", "clock"}} { - test.WithWd(path.Join(test.GetProjectDir(), data.pkg), func(origWd string) { - exec.Command("go", "get", "-d").Run() - goBuildFlags := []string{"build", "-buildmode=plugin", "-o=" + tmpDir.Path(fmt.Sprintf("%s.so", data.name))} - - goBuildFlags = append(goBuildFlags, extraGoBuildFlags...) - - cmd := exec.Command("go", goBuildFlags...) - cmd.Stderr = os.Stderr - assert.Nil(t, cmd.Run()) - }) - db := testdb.NewDBWithDefaultUser(t) - db.GormDatabase.DB.Callback().Create().Register("no_create", func(s *gorm.Scope) { - if s.TableName() == data.faultyTable { - s.Err(errors.New("database failed")) - } - }) - _, err := NewManager(db, tmpDir.Path(), nil, nil) - assert.Error(t, err) - os.Remove(tmpDir.Path(fmt.Sprintf("%s.so", data.name))) - } -} - func TestNewManager_InternalApplicationManagement(t *testing.T) { db := testdb.NewDBWithDefaultUser(t) diff --git a/plugin/manager_test_race.go b/plugin/manager_test_race.go index 4715167d..ff558d76 100644 --- a/plugin/manager_test_race.go +++ b/plugin/manager_test_race.go @@ -1,3 +1,4 @@ +//go:build race // +build race package plugin diff --git a/plugin/testing/mock/mock.go b/plugin/testing/mock/mock.go index c4165215..10c74849 100644 --- a/plugin/testing/mock/mock.go +++ b/plugin/testing/mock/mock.go @@ -58,8 +58,10 @@ type PluginConfig struct { IsNotValid bool } -var disableFailUsers = make(map[uint]error) -var enableFailUsers = make(map[uint]error) +var ( + disableFailUsers = make(map[uint]error) + enableFailUsers = make(map[uint]error) +) // ReturnErrorOnEnableForUser registers a uid which will throw an error on enabling. func ReturnErrorOnEnableForUser(uid uint, err error) { diff --git a/runner/runner.go b/runner/runner.go index f985a6c8..c1ce239c 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -111,7 +111,7 @@ func (l *LoggingRoundTripper) RoundTrip(r *http.Request) (resp *http.Response, e } else if err != nil { log.Printf("%s Request Failed: %s on %s %s\n", l.Name, err.Error(), r.Method, r.URL.String()) } - return + return resp, err } func applyLetsEncrypt(s *http.Server, conf *config.Configuration) {