diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 57959e1..58ca2fc 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,9 +8,9 @@ jobs: steps: - name: Set up Go 1.16 - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: - go-version: 1.16.x + go-version: '^1.18' id: go - name: Check out code into the Go module directory diff --git a/bot/bot.go b/bot/bot.go index b43995f..baeadfa 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -4,6 +4,7 @@ package bot import ( "fmt" + bh "github.com/timshannon/bolthold" "math/rand" "net/http" "reflect" @@ -13,7 +14,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot/history" "github.com/velour/catbase/bot/msg" @@ -111,8 +111,6 @@ func New(config *config.Config, connector Connector) Bot { history: history.New(historySz), } - bot.migrateDB() - bot.RefreshPluginBlacklist() bot.RefreshPluginWhitelist() @@ -161,33 +159,13 @@ func (b *bot) Config() *config.Config { return b.config } -func (b *bot) DB() *sqlx.DB { - return b.config.DB -} +// TODO: remove +//func (b *bot) DB() *sqlx.DB { +// return b.config.DB() +//} -// Create any tables if necessary based on version of DB -// Plugins should create their own tables, these are only for official bot stuff -// Note: This does not return an error. Database issues are all fatal at this stage. -func (b *bot) migrateDB() { - if _, err := b.DB().Exec(`create table if not exists variables ( - id integer primary key, - name string, - value string - );`); err != nil { - log.Fatal().Err(err).Msgf("Initial db migration create variables table") - } - if _, err := b.DB().Exec(`create table if not exists pluginBlacklist ( - channel string, - name string, - primary key (channel, name) - );`); err != nil { - log.Fatal().Err(err).Msgf("Initial db migration create blacklist table") - } - if _, err := b.DB().Exec(`create table if not exists pluginWhitelist ( - name string primary key - );`); err != nil { - log.Fatal().Err(err).Msgf("Initial db migration create whitelist table") - } +func (b *bot) Store() *bh.Store { + return b.config.Store() } // Adds a constructed handler to the bots handlers list @@ -330,13 +308,19 @@ func (b *bot) SetQuiet(status bool) { b.quiet = status } +type blacklistItem struct { + Channel string + Name string +} + +type whitelistItem struct { + Name string +} + // RefreshPluginBlacklist loads data for which plugins are disabled for particular channels func (b *bot) RefreshPluginBlacklist() error { - blacklistItems := []struct { - Channel string - Name string - }{} - if err := b.DB().Select(&blacklistItems, `select channel, name from pluginBlacklist`); err != nil { + blacklistItems := []blacklistItem{} + if err := b.Store().Find(&blacklistItems, &bh.Query{}); err != nil { return fmt.Errorf("%w", err) } b.pluginBlacklist = make(map[string]bool) @@ -349,12 +333,10 @@ func (b *bot) RefreshPluginBlacklist() error { // RefreshPluginWhitelist loads data for which plugins are enabled func (b *bot) RefreshPluginWhitelist() error { - whitelistItems := []struct { - Name string - }{ + whitelistItems := []whitelistItem{ {Name: "admin"}, // we must always ensure admin is on! } - if err := b.DB().Select(&whitelistItems, `select name from pluginWhitelist`); err != nil { + if err := b.Store().Find(&whitelistItems, &bh.Query{}); err != nil { return fmt.Errorf("%w", err) } b.pluginWhitelist = make(map[string]bool) @@ -411,9 +393,8 @@ func (b *bot) CheckPassword(secret, password string) bool { secret = parts[0] password = parts[1] } - q := `select encoded_pass from apppass where secret = ?` encodedPasswords := [][]byte{} - b.DB().Select(&encodedPasswords, q, secret) + b.Store().Find(&encodedPasswords, bh.Where("secret").Eq(secret)) for _, p := range encodedPasswords { if err := bcrypt.CompareHashAndPassword(p, []byte(password)); err == nil { return true diff --git a/bot/handlers.go b/bot/handlers.go index 4464621..4797862 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -4,10 +4,10 @@ package bot import ( "bytes" - "database/sql" "encoding/json" "errors" "fmt" + bh "github.com/timshannon/bolthold" "io/ioutil" "math/rand" "net/http" @@ -216,27 +216,33 @@ func (b *bot) Filter(message msg.Message, input string) string { return input } +type variable struct { + Text string +} + func (b *bot) getVar(varName string) (string, error) { - var text string - err := b.DB().Get(&text, `select value from variables where name=? order by random() limit 1`, varName) - switch { - case err == sql.ErrNoRows: - return "", fmt.Errorf("No factoid found") - case err != nil: + var v []variable + err := b.Store().Find(&v, bh.Where("name").Eq(varName)) + if err != nil { log.Fatal().Err(err).Msg("getVar error") } - return text, nil + entry := v[rand.Intn(len(v))] + return entry.Text, nil } func (b *bot) listVars(conn Connector, channel string, parts []string) { - var variables []string - err := b.DB().Select(&variables, `select name from variables group by name`) + var variables []variable + err := b.Store().Find(&variables, &bh.Query{}) if err != nil { log.Fatal().Err(err) } msg := "I know: $who, $someone, $digit, $nonzero, $time, $now, $msg" - if len(variables) > 0 { - msg += ", " + strings.Join(variables, ", ") + out := []string{} + for _, v := range variables { + out = append(out, v.Text) + } + if len(out) > 0 { + msg += ", " + strings.Join(out, ", ") } b.Send(conn, Message, channel, msg) } diff --git a/bot/interfaces.go b/bot/interfaces.go index 57ca2a8..e5bcfa0 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -3,11 +3,10 @@ package bot import ( + bh "github.com/timshannon/bolthold" "net/http" "regexp" - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/user" "github.com/velour/catbase/config" @@ -81,7 +80,8 @@ type Bot interface { Config() *config.Config // DB gives access to the current database - DB() *sqlx.DB + //DB() *sqlx.DB + Store() *bh.Store // Who lists users in a particular channel // The channel should be represented as an ID for slack (check the connector for details) diff --git a/bot/mock.go b/bot/mock.go index 736a085..c2b0a59 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -29,7 +29,7 @@ type MockBot struct { } func (mb *MockBot) Config() *config.Config { return mb.Cfg } -func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB } +func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB() } func (mb *MockBot) Who(string) []user.User { return []user.User{} } func (mb *MockBot) WhoAmI() string { return "tester" } func (mb *MockBot) DefaultConnector() Connector { return nil } diff --git a/config/config.go b/config/config.go index a9d58a2..d45e46e 100644 --- a/config/config.go +++ b/config/config.go @@ -3,15 +3,15 @@ package config import ( - "database/sql" "encoding/json" "fmt" "os" - "regexp" "strconv" "strings" - sqlite3 "github.com/mattn/go-sqlite3" + _ "modernc.org/sqlite" + + bh "github.com/timshannon/bolthold" "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" @@ -20,20 +20,34 @@ import ( // Config stores any system-wide startup information that cannot be easily configured via // the database type Config struct { - *sqlx.DB + db *sqlx.DB + + store *bh.Store DBFile string secrets map[string]Secret } -// Secret is a config value that is loaded permanently and not ever displayed -type Secret struct { +// Value is a config value that is loaded permanently and not ever displayed +type Value struct { // Key is the key field of the table Key string `db:"key"` // Value represents the secret that must not be shared Value string `db:"value"` } +// Secret is a separate type (for storage differentiation) +type Secret Value + +// DB returns the SQL database instance +func (c *Config) DB() *sqlx.DB { + return c.db +} + +func (c *Config) Store() *bh.Store { + return c.store +} + // GetFloat64 returns the config value for a string key // It will first look in the env vars for the key // It will check the db for the key if an env DNE @@ -97,14 +111,13 @@ func (c *Config) GetString(key, fallback string) string { if v, found := c.secrets[key]; found { return v.Value } - var configValue string - q := `select value from config where key=?` - err := c.DB.Get(&configValue, q, key) + var configValue Value + err := c.store.Get(key, &configValue) if err != nil { log.Debug().Msgf("WARN: Key %s is empty", key) return fallback } - return configValue + return configValue.Value } func (c *Config) GetMap(key string, fallback map[string]string) map[string]string { @@ -136,20 +149,8 @@ func (c *Config) GetArray(key string, fallback []string) []string { } func (c *Config) Unset(key string) error { - q := `delete from config where key=?` - tx, err := c.Begin() - if err != nil { - return err - } - _, err = tx.Exec(q, key) - if err != nil { - return err - } - err = tx.Commit() - if err != nil { - return err - } - return nil + err := c.store.Delete(key, &Value{}) + return err } // Set changes the value for a configuration in the database @@ -157,27 +158,14 @@ func (c *Config) Unset(key string) error { func (c *Config) Set(key, value string) error { key = strings.ToLower(key) value = strings.Trim(value, "`") - q := `insert into config (key,value) values (?, ?) - on conflict(key) do update set value=?;` - tx, err := c.Begin() - if err != nil { - return err - } - _, err = tx.Exec(q, key, value, value) - if err != nil { - return err - } - err = tx.Commit() - if err != nil { - return err - } - return nil + + err := c.store.Update(key, Value{key, value}) + return err } func (c *Config) RefreshSecrets() error { - q := `select key, value from secrets` var secrets []Secret - err := c.Select(&secrets, q) + err := c.store.Find(&secrets, &bh.Query{}) if err != nil { return err } @@ -214,36 +202,43 @@ func (c *Config) SetArray(key string, values []string) error { return c.Set(key, vals) } -func init() { - regex := func(re, s string) (bool, error) { - return regexp.MatchString(re, s) - } - sql.Register("sqlite3_custom", - &sqlite3.SQLiteDriver{ - ConnectHook: func(conn *sqlite3.SQLiteConn) error { - return conn.RegisterFunc("REGEXP", regex, true) - }, - }) -} +//func init() { +// regex := func(re, s string) (bool, error) { +// return regexp.MatchString(re, s) +// } +// sql.Register("sqlite3_custom", +// &sqlite3.SQLiteDriver{ +// ConnectHook: func(conn *sqlite3.SQLiteConn) error { +// return conn.RegisterFunc("REGEXP", regex, true) +// }, +// }) +//} // Readconfig loads the config data out of a JSON file located in cfile func ReadConfig(dbpath string) *Config { if dbpath == "" { dbpath = "catbase.db" } + + store, err := bh.Open(strings.ReplaceAll(dbpath, ".db", ".store"), 0666, nil) + if err != nil { + log.Fatal().Err(err).Msgf("could not open bolthold") + } + log.Info().Msgf("Using %s as database file.\n", dbpath) - sqlDB, err := sqlx.Open("sqlite3_custom", dbpath) + sqlDB, err := sqlx.Open("sqlite", dbpath) if err != nil { - log.Fatal().Err(err) + log.Fatal().Err(err).Msgf("could not open sqlite") } c := Config{ DBFile: dbpath, secrets: map[string]Secret{}, + store: store, } - c.DB = sqlDB + c.db = sqlDB - if _, err := c.Exec(`create table if not exists config ( + if _, err := c.db.Exec(`create table if not exists config ( key string, value string, primary key (key) @@ -251,7 +246,7 @@ func ReadConfig(dbpath string) *Config { log.Fatal().Err(err).Msgf("failed to initialize config") } - if _, err := c.Exec(`create table if not exists secrets ( + if _, err := c.db.Exec(`create table if not exists secrets ( key string, value string, primary key (key) @@ -265,5 +260,60 @@ func ReadConfig(dbpath string) *Config { log.Info().Msgf("catbase is running.") + if err := c.migrate(); err != nil { + log.Fatal().Err(err).Msgf("could not migrate") + } + return &c } + +func (c *Config) migrate() error { + // check countSqliteEntries of config/secrets in sqlite + var countSqliteEntries int64 + err := c.db.Get(&countSqliteEntries, "select count(*) as countSqliteEntries from config") + if err != nil { + return err + } + + countBoltEntries, err := c.store.Count(Value{}, &bh.Query{}) + if err != nil { + return err + } + + countBoltSecrets, err := c.store.Count(Secret{}, &bh.Query{}) + if err != nil { + return err + } + + log.Debug().Msgf("Found %d sqlite entries and %d bolt entries and %d bolt secrets", countSqliteEntries, countBoltEntries, countBoltSecrets) + + if countSqliteEntries == 0 || countBoltEntries > 0 { + return nil + } + + var items []Value + c.db.Select(&items, "select key, value from config") + + log.Debug().Msgf("%d entries to migrate", len(items)) + + c.RefreshSecrets() + secrets := c.GetAllSecrets() + + // add all to the bolthold + + for _, it := range items { + err := c.store.Insert(it.Key, it) + if err != nil { + return err + } + } + + for _, s := range secrets { + err := c.store.Insert(s.Key, s) + if err != nil { + return err + } + } + + return nil +} diff --git a/config/defaults.go b/config/defaults.go index 26626ba..d2b683c 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -32,7 +32,7 @@ func (c *Config) SetDefaults(mainChannel, nick string) { } var buf bytes.Buffer t.Execute(&buf, vals) - c.MustExec(`delete from config;`) - c.MustExec(buf.String()) + c.db.MustExec(`delete from config;`) + c.db.MustExec(buf.String()) log.Info().Msgf("Configuration initialized.") } diff --git a/go.mod b/go.mod index c5be57f..7327018 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect - github.com/google/uuid v1.1.1 + github.com/google/uuid v1.3.0 github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect github.com/itchyny/gojq v0.12.3 github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6 @@ -41,7 +41,6 @@ require ( github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852 github.com/mattn/go-colorable v0.1.6 // indirect - github.com/mattn/go-sqlite3 v1.14.8 github.com/mmcdole/gofeed v1.0.0-beta2 github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -55,6 +54,7 @@ require ( github.com/stretchr/objx v0.2.0 // indirect github.com/stretchr/testify v1.4.0 github.com/temoto/robotstxt v1.1.1 // indirect + github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a github.com/trubitsyn/go-zero-width v1.0.1 github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/libphonenumber v1.1.0 // indirect @@ -62,7 +62,6 @@ require ( golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect - golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect golang.org/x/text v0.3.7 // indirect gonum.org/v1/gonum v0.6.0 // indirect @@ -70,4 +69,5 @@ require ( gopkg.in/go-playground/webhooks.v5 v5.13.0 gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + modernc.org/sqlite v1.14.2 ) diff --git a/go.sum b/go.sum index 78a74b7..a7dcacb 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI= github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw= github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad h1:Qk76DOWdOp+GlyDKBAG3Klr9cn7N+LcYc82AZ2S7+cA= @@ -68,9 +70,11 @@ github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg= github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M= github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -88,6 +92,8 @@ github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7/go.mod h1:G6Ec github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a h1:Z7+SSApKiwPjNic+NF9+j7h657Uyvdp/jA3iTKhpj4E= @@ -106,8 +112,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU= -github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mmcdole/gofeed v1.0.0-beta2 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E= github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= @@ -122,6 +128,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4= github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -142,6 +150,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/temoto/robotstxt v1.1.1 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA= github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= +github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a h1:oIi7H/bwFUYKYhzKbHc+3MvHRWqhQwXVB4LweLMiVy0= +github.com/timshannon/bolthold v0.0.0-20210913165410-232392fc8a6a/go.mod h1:iSvujNDmpZ6eQX+bg/0X3lF7LEmZ8N77g2a/J/+Zt2U= github.com/trubitsyn/go-zero-width v1.0.1 h1:AAZhtyGXW79T5BouAF0R9FtDhGcp7IGbLZo2Id3N+m8= github.com/trubitsyn/go-zero-width v1.0.1/go.mod h1:gGhBV4CZHjqXBYSgaxTCKZj+dXJndhdm1zAtAChtIUI= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= @@ -150,10 +160,15 @@ github.com/ttacon/libphonenumber v1.1.0 h1:tC6kE4t8UI4OqQVQjW5q8gSWhG2wnY5moEpSE github.com/ttacon/libphonenumber v1.1.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M= github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw= github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -168,12 +183,15 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDA golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0= golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -186,15 +204,21 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -203,8 +227,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= @@ -223,4 +253,117 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.9/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.11/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.34.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.0/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.4/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.5/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.8/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.10/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.15/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.16/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.17/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.35.18 h1:rMZhRcWrba0y3nVmdiQ7kxAgOOSq2m2f2VzjHLgEs6U= +modernc.org/cc/v3 v3.35.18/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.10.0/go.mod h1:c0yBmkRFi7uW4J7fwx/JiijwOjeAeR2NoSaRVFPmjMw= +modernc.org/ccgo/v3 v3.11.0/go.mod h1:dGNposbDp9TOZ/1KBxghxtUp/bzErD0/0QW4hhSaBMI= +modernc.org/ccgo/v3 v3.11.1/go.mod h1:lWHxfsn13L3f7hgGsGlU28D9eUOf6y3ZYHKoPaKU0ag= +modernc.org/ccgo/v3 v3.11.3/go.mod h1:0oHunRBMBiXOKdaglfMlRPBALQqsfrCKXgw9okQ3GEw= +modernc.org/ccgo/v3 v3.12.4/go.mod h1:Bk+m6m2tsooJchP/Yk5ji56cClmN6R1cqc9o/YtbgBQ= +modernc.org/ccgo/v3 v3.12.6/go.mod h1:0Ji3ruvpFPpz+yu+1m0wk68pdr/LENABhTrDkMDWH6c= +modernc.org/ccgo/v3 v3.12.8/go.mod h1:Hq9keM4ZfjCDuDXxaHptpv9N24JhgBZmUG5q60iLgUo= +modernc.org/ccgo/v3 v3.12.11/go.mod h1:0jVcmyDwDKDGWbcrzQ+xwJjbhZruHtouiBEvDfoIsdg= +modernc.org/ccgo/v3 v3.12.14/go.mod h1:GhTu1k0YCpJSuWwtRAEHAol5W7g1/RRfS4/9hc9vF5I= +modernc.org/ccgo/v3 v3.12.18/go.mod h1:jvg/xVdWWmZACSgOiAhpWpwHWylbJaSzayCqNOJKIhs= +modernc.org/ccgo/v3 v3.12.20/go.mod h1:aKEdssiu7gVgSy/jjMastnv/q6wWGRbszbheXgWRHc8= +modernc.org/ccgo/v3 v3.12.21/go.mod h1:ydgg2tEprnyMn159ZO/N4pLBqpL7NOkJ88GT5zNU2dE= +modernc.org/ccgo/v3 v3.12.22/go.mod h1:nyDVFMmMWhMsgQw+5JH6B6o4MnZ+UQNw1pp52XYFPRk= +modernc.org/ccgo/v3 v3.12.25/go.mod h1:UaLyWI26TwyIT4+ZFNjkyTbsPsY3plAEB6E7L/vZV3w= +modernc.org/ccgo/v3 v3.12.29/go.mod h1:FXVjG7YLf9FetsS2OOYcwNhcdOLGt8S9bQ48+OP75cE= +modernc.org/ccgo/v3 v3.12.36/go.mod h1:uP3/Fiezp/Ga8onfvMLpREq+KUjUmYMxXPO8tETHtA8= +modernc.org/ccgo/v3 v3.12.38/go.mod h1:93O0G7baRST1vNj4wnZ49b1kLxt0xCW5Hsa2qRaZPqc= +modernc.org/ccgo/v3 v3.12.43/go.mod h1:k+DqGXd3o7W+inNujK15S5ZYuPoWYLpF5PYougCmthU= +modernc.org/ccgo/v3 v3.12.46/go.mod h1:UZe6EvMSqOxaJ4sznY7b23/k13R8XNlyWsO5bAmSgOE= +modernc.org/ccgo/v3 v3.12.47/go.mod h1:m8d6p0zNps187fhBwzY/ii6gxfjob1VxWb919Nk1HUk= +modernc.org/ccgo/v3 v3.12.50/go.mod h1:bu9YIwtg+HXQxBhsRDE+cJjQRuINuT9PUK4orOco/JI= +modernc.org/ccgo/v3 v3.12.51/go.mod h1:gaIIlx4YpmGO2bLye04/yeblmvWEmE4BBBls4aJXFiE= +modernc.org/ccgo/v3 v3.12.53/go.mod h1:8xWGGTFkdFEWBEsUmi+DBjwu/WLy3SSOrqEmKUjMeEg= +modernc.org/ccgo/v3 v3.12.54/go.mod h1:yANKFTm9llTFVX1FqNKHE0aMcQb1fuPJx6p8AcUx+74= +modernc.org/ccgo/v3 v3.12.55/go.mod h1:rsXiIyJi9psOwiBkplOaHye5L4MOOaCjHg1Fxkj7IeU= +modernc.org/ccgo/v3 v3.12.56/go.mod h1:ljeFks3faDseCkr60JMpeDb2GSO3TKAmrzm7q9YOcMU= +modernc.org/ccgo/v3 v3.12.57/go.mod h1:hNSF4DNVgBl8wYHpMvPqQWDQx8luqxDnNGCMM4NFNMc= +modernc.org/ccgo/v3 v3.12.60/go.mod h1:k/Nn0zdO1xHVWjPYVshDeWKqbRWIfif5dtsIOCUVMqM= +modernc.org/ccgo/v3 v3.12.65/go.mod h1:D6hQtKxPNZiY6wDBtehSGKFKmyXn53F8nGTpH+POmS4= +modernc.org/ccgo/v3 v3.12.66/go.mod h1:jUuxlCFZTUZLMV08s7B1ekHX5+LIAurKTTaugUr/EhQ= +modernc.org/ccgo/v3 v3.12.67/go.mod h1:Bll3KwKvGROizP2Xj17GEGOTrlvB1XcVaBrC90ORO84= +modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3cQ= +modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY= +modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8= +modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w= +modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA= +modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/libc v1.11.0/go.mod h1:2lOfPmj7cz+g1MrPNmX65QCzVxgNq2C5o0jdLY2gAYg= +modernc.org/libc v1.11.2/go.mod h1:ioIyrl3ETkugDO3SGZ+6EOKvlP3zSOycUETe4XM4n8M= +modernc.org/libc v1.11.5/go.mod h1:k3HDCP95A6U111Q5TmG3nAyUcp3kR5YFZTeDS9v8vSU= +modernc.org/libc v1.11.6/go.mod h1:ddqmzR6p5i4jIGK1d/EiSw97LBcE3dK24QEwCFvgNgE= +modernc.org/libc v1.11.11/go.mod h1:lXEp9QOOk4qAYOtL3BmMve99S5Owz7Qyowzvg6LiZso= +modernc.org/libc v1.11.13/go.mod h1:ZYawJWlXIzXy2Pzghaf7YfM8OKacP3eZQI81PDLFdY8= +modernc.org/libc v1.11.16/go.mod h1:+DJquzYi+DMRUtWI1YNxrlQO6TcA5+dRRiq8HWBWRC8= +modernc.org/libc v1.11.19/go.mod h1:e0dgEame6mkydy19KKaVPBeEnyJB4LGNb0bBH1EtQ3I= +modernc.org/libc v1.11.24/go.mod h1:FOSzE0UwookyT1TtCJrRkvsOrX2k38HoInhw+cSCUGk= +modernc.org/libc v1.11.26/go.mod h1:SFjnYi9OSd2W7f4ct622o/PAYqk7KHv6GS8NZULIjKY= +modernc.org/libc v1.11.27/go.mod h1:zmWm6kcFXt/jpzeCgfvUNswM0qke8qVwxqZrnddlDiE= +modernc.org/libc v1.11.28/go.mod h1:Ii4V0fTFcbq3qrv3CNn+OGHAvzqMBvC7dBNyC4vHZlg= +modernc.org/libc v1.11.31/go.mod h1:FpBncUkEAtopRNJj8aRo29qUiyx5AvAlAxzlx9GNaVM= +modernc.org/libc v1.11.34/go.mod h1:+Tzc4hnb1iaX/SKAutJmfzES6awxfU1BPvrrJO0pYLg= +modernc.org/libc v1.11.37/go.mod h1:dCQebOwoO1046yTrfUE5nX1f3YpGZQKNcITUYWlrAWo= +modernc.org/libc v1.11.39/go.mod h1:mV8lJMo2S5A31uD0k1cMu7vrJbSA3J3waQJxpV4iqx8= +modernc.org/libc v1.11.42/go.mod h1:yzrLDU+sSjLE+D4bIhS7q1L5UwXDOw99PLSX0BlZvSQ= +modernc.org/libc v1.11.44/go.mod h1:KFq33jsma7F5WXiYelU8quMJasCCTnHK0mkri4yPHgA= +modernc.org/libc v1.11.45/go.mod h1:Y192orvfVQQYFzCNsn+Xt0Hxt4DiO4USpLNXBlXg/tM= +modernc.org/libc v1.11.47/go.mod h1:tPkE4PzCTW27E6AIKIR5IwHAQKCAtudEIeAV1/SiyBg= +modernc.org/libc v1.11.49/go.mod h1:9JrJuK5WTtoTWIFQ7QjX2Mb/bagYdZdscI3xrvHbXjE= +modernc.org/libc v1.11.51/go.mod h1:R9I8u9TS+meaWLdbfQhq2kFknTW0O3aw3kEMqDDxMaM= +modernc.org/libc v1.11.53/go.mod h1:5ip5vWYPAoMulkQ5XlSJTy12Sz5U6blOQiYasilVPsU= +modernc.org/libc v1.11.54/go.mod h1:S/FVnskbzVUrjfBqlGFIPA5m7UwB3n9fojHhCNfSsnw= +modernc.org/libc v1.11.55/go.mod h1:j2A5YBRm6HjNkoSs/fzZrSxCuwWqcMYTDPLNx0URn3M= +modernc.org/libc v1.11.56/go.mod h1:pakHkg5JdMLt2OgRadpPOTnyRXm/uzu+Yyg/LSLdi18= +modernc.org/libc v1.11.58/go.mod h1:ns94Rxv0OWyoQrDqMFfWwka2BcaF6/61CqJRK9LP7S8= +modernc.org/libc v1.11.70/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.71/go.mod h1:DUOmMYe+IvKi9n6Mycyx3DbjfzSKrdr/0Vgt3j7P5gw= +modernc.org/libc v1.11.75/go.mod h1:dGRVugT6edz361wmD9gk6ax1AbDSe0x5vji0dGJiPT0= +modernc.org/libc v1.11.82/go.mod h1:NF+Ek1BOl2jeC7lw3a7Jj5PWyHPwWD4aq3wVKxqV1fI= +modernc.org/libc v1.11.86/go.mod h1:ePuYgoQLmvxdNT06RpGnaDKJmDNEkV7ZPKI2jnsvZoE= +modernc.org/libc v1.11.87 h1:PzIzOqtlzMDDcCzJ5cUP6h/Ku6Fa9iyflP2ccTY64aE= +modernc.org/libc v1.11.87/go.mod h1:Qvd5iXTeLhI5PS0XSyqMY99282y+3euapQFxM7jYnpY= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/memory v1.0.5 h1:XRch8trV7GgvTec2i7jc33YlUI0RKVDBvZ5eZ5m8y14= +modernc.org/memory v1.0.5/go.mod h1:B7OYswTRnfGg+4tDH1t1OeUNnsy2viGTdME4tzd+IjM= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE= +modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.8.13 h1:V0sTNBw0Re86PvXZxuCub3oO9WrSTqALgrwNZNvLFGw= +modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.2.19 h1:BGyRFWhDVn5LFS5OcX4Yd/MlpRTOc7hOPTdcIpCiUao= +modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/plugins/achievements/achievements.go b/plugins/achievements/achievements.go index 7a737c3..2bd1da2 100644 --- a/plugins/achievements/achievements.go +++ b/plugins/achievements/achievements.go @@ -2,11 +2,11 @@ package achievements import ( "fmt" + bh "github.com/timshannon/bolthold" "regexp" "strings" "time" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" @@ -16,20 +16,16 @@ import ( // A plugin to track our misdeeds type AchievementsPlugin struct { - bot bot.Bot - cfg *config.Config - db *sqlx.DB + bot bot.Bot + cfg *config.Config + store *bh.Store } func New(b bot.Bot) *AchievementsPlugin { ap := &AchievementsPlugin{ - bot: b, - cfg: b.Config(), - db: b.DB(), - } - err := ap.mkDB() - if err != nil { - log.Fatal().Err(err).Msg("unable to create achievements tables") + bot: b, + cfg: b.Config(), + store: b.Store(), } ap.register() @@ -50,41 +46,10 @@ func (p *AchievementsPlugin) register() { p.bot.Register(p, bot.Help, p.help) } -func (p *AchievementsPlugin) mkDB() error { - trophiesTable := `create table if not exists trophies ( - emojy string primary key, - description string, - creator string - );` - - awardsTable := `create table if not exists awards ( - id integer primary key, - emojy string references trophies(emojy) on delete restrict on update cascade, - holder string, - amount integer default 0, - granted timestamp CURRENT_TIMESTAMP - );` - tx, err := p.db.Beginx() - if err != nil { - return err - } - if _, err = tx.Exec(trophiesTable); err != nil { - return err - } - if _, err = tx.Exec(awardsTable); err != nil { - return err - } - err = tx.Commit() - if err != nil { - return err - } - return nil -} - func (p *AchievementsPlugin) GetAwards(nick string) []Award { var awards []Award - q := `select * from awards inner join trophies on awards.emojy=trophies.emojy where holder=?` - if err := p.db.Select(&awards, q, nick); err != nil { + err := p.store.Find(&awards, bh.Where("holder").Eq(nick)) + if err != nil { log.Error().Err(err).Msg("could not select awards") } return awards @@ -206,20 +171,21 @@ func (p *AchievementsPlugin) help(c bot.Connector, kind bot.Kind, message msg.Me // Award is used by other plugins to register a particular award for a user func (p *AchievementsPlugin) Grant(nick, emojy string) (Award, error) { - empty := Award{} - q := `insert into awards (emojy,holder) values (?, ?)` - tx, err := p.db.Beginx() + trophy, err := p.FindTrophy(emojy) if err != nil { - return empty, err + return Award{}, err } - if _, err := tx.Exec(q, emojy, nick); err != nil { - tx.Rollback() - return empty, err + award := Award{ + Holder: nick, + Emojy: emojy, + Description: trophy.Description, + Granted: time.Now(), } - if err := tx.Commit(); err != nil { - return empty, err + if err = p.store.Insert(bh.NextSequence(), &award); err != nil { + return Award{}, err } - return p.FindAward(emojy) + + return award, err } func (p *AchievementsPlugin) Create(emojy, description, creator string) (Trophy, error) { @@ -228,22 +194,14 @@ func (p *AchievementsPlugin) Create(emojy, description, creator string) (Trophy, return t, fmt.Errorf("the trophy %s already exists", emojy) } - q := `insert into trophies (emojy,description,creator) values (?,?,?)` - tx, err := p.db.Beginx() - if err != nil { - return Trophy{}, err - } - _, err = tx.Exec(q, emojy, description, creator) - if err != nil { - tx.Rollback() - return Trophy{}, err - } - err = tx.Commit() - return Trophy{ + t = Trophy{ Emojy: emojy, Description: description, Creator: creator, - }, err + } + + err = p.store.Insert(emojy, t) + return t, err } type Trophy struct { @@ -253,34 +211,21 @@ type Trophy struct { } type Award struct { - Trophy - ID int64 - Holder string - Amount int - Granted *time.Time -} - -func (a *Award) Save() error { - return nil + ID int64 `boltholderid:"ID"` + Holder string + Emojy string + Description string + Granted time.Time } func (p *AchievementsPlugin) AllTrophies() ([]Trophy, error) { - q := `select * from trophies order by creator` var t []Trophy - err := p.db.Select(&t, q) + err := p.store.Find(&t, &bh.Query{}) return t, err } func (p *AchievementsPlugin) FindTrophy(emojy string) (Trophy, error) { - q := `select * from trophies where emojy=?` var t Trophy - err := p.db.Get(&t, q, emojy) + err := p.store.Find(&t, bh.Where("emojy").Eq(emojy)) return t, err } - -func (p *AchievementsPlugin) FindAward(emojy string) (Award, error) { - q := `select * from awards inner join trophies on awards.emojy=trophies.emojy where trophies.emojy=?` - var a Award - err := p.db.Get(&a, q, emojy) - return a, err -} diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go index 9490cf0..78fe744 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -4,6 +4,7 @@ package admin import ( "fmt" + bh "github.com/timshannon/bolthold" "os" "regexp" "strings" @@ -11,8 +12,6 @@ import ( "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/config" @@ -21,9 +20,9 @@ import ( // This is a admin plugin to serve as an example and quick copy/paste for new plugins. type AdminPlugin struct { - bot bot.Bot - db *sqlx.DB - cfg *config.Config + bot bot.Bot + store *bh.Store + cfg *config.Config quiet bool } @@ -31,13 +30,11 @@ type AdminPlugin struct { // New creates a new AdminPlugin with the Plugin interface func New(b bot.Bot) *AdminPlugin { p := &AdminPlugin{ - bot: b, - db: b.DB(), - cfg: b.Config(), + bot: b, + store: b.Store(), + cfg: b.Config(), } - p.mkDB() - b.RegisterRegex(p, bot.Message, comeBackRegex, p.comeBackCmd) b.RegisterRegexCmd(p, bot.Message, shutupRegex, p.shutupCmd) b.RegisterRegexCmd(p, bot.Message, addBlacklistRegex, p.isAdmin(p.addBlacklistCmd)) @@ -72,16 +69,6 @@ var forbiddenKeys = map[string]bool{ "meme.memes": true, } -func (p *AdminPlugin) mkDB() { - q := `create table if not exists apppass ( - id integer primary key autoincrement, - secret string not null, - encoded_pass string not null, - cost integer default 10 - )` - p.db.MustExec(q) -} - var shutupRegex = regexp.MustCompile(`(?i)^shut up$`) var comeBackRegex = regexp.MustCompile(`(?i)^come back$`) @@ -243,12 +230,11 @@ func (p *AdminPlugin) passwordCmd(r bot.Request) bool { } func (p *AdminPlugin) variableSetCmd(r bot.Request) bool { - variable := strings.ToLower(r.Values["var"]) + key := strings.ToLower(r.Values["var"]) value := r.Values["value"] + variable := bot.Variable{key, value} - var count int64 - row := p.db.QueryRow(`select count(*) from variables where value = ?`, variable, value) - err := row.Scan(&count) + count, err := p.store.Count(bot.Variable{}, bh.Where("value").Eq(value)) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") log.Error().Err(err) @@ -258,7 +244,7 @@ func (p *AdminPlugin) variableSetCmd(r bot.Request) bool { if count > 0 { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I've already got that one.") } else { - _, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value) + err := p.store.Insert(bh.NextSequence(), variable) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") log.Error().Err(err) @@ -273,7 +259,8 @@ func (p *AdminPlugin) variableUnSetCmd(r bot.Request) bool { variable := strings.ToLower(r.Values["var"]) value := r.Values["value"] - _, err := p.db.Exec(`delete from variables where name=? and value=?`, variable, value) + err := p.store.Delete(variable, bot.Variable{}) + p.store.DeleteMatching(bot.Variable{}, bh.Where("name").Eq(variable).And("value").Eq(value)) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") log.Error().Err(err) @@ -390,9 +377,10 @@ func (p *AdminPlugin) modList(query, channel, plugin string) error { plugins := p.bot.GetPluginNames() for _, pp := range plugins { if pp == plugin { - if _, err := p.db.Exec(query, channel, plugin); err != nil { - return fmt.Errorf("%w", err) - } + // todo: yikes + //if _, err := p.db.Exec(query, channel, plugin); err != nil { + // return fmt.Errorf("%w", err) + //} err := p.bot.RefreshPluginWhitelist() if err != nil { return fmt.Errorf("%w", err) diff --git a/plugins/admin/web.go b/plugins/admin/web.go index b20f143..85d815f 100644 --- a/plugins/admin/web.go +++ b/plugins/admin/web.go @@ -6,6 +6,8 @@ import ( "embed" "encoding/json" "fmt" + bh "github.com/timshannon/bolthold" + "github.com/velour/catbase/config" "io/ioutil" "net/http" "strings" @@ -106,11 +108,18 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) { } req.PassEntry.Pass = fmt.Sprintf("%x", md5.Sum(b)) } - q := `insert into apppass (secret, encoded_pass, cost) values (?, ?, ?)` + req.PassEntry.EncodePass() check := bcrypt.CompareHashAndPassword(req.PassEntry.encodedPass, []byte(req.PassEntry.Pass)) + entry := PassEntry{ + Secret: req.PassEntry.Secret, + encodedPass: req.PassEntry.encodedPass, + Cost: req.PassEntry.Cost, + Pass: "", + } + log.Debug(). Str("secret", req.PassEntry.Secret). Str("encoded", string(req.PassEntry.encodedPass)). @@ -118,17 +127,11 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) { Interface("check", check). Msg("debug pass creation") - res, err := p.db.Exec(q, req.PassEntry.Secret, req.PassEntry.encodedPass, req.PassEntry.Cost) + err := p.store.Insert(bh.NextSequence(), &entry) if err != nil { writeErr(w, err) return } - id, err := res.LastInsertId() - if err != nil { - writeErr(w, err) - return - } - req.PassEntry.ID = id j, _ := json.Marshal(req.PassEntry) fmt.Fprint(w, string(j)) return @@ -137,16 +140,14 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) { writeErr(w, fmt.Errorf("missing ID")) return } - q := `delete from apppass where id = ?` - _, err := p.db.Exec(q, req.PassEntry.ID) + err := p.store.Delete(req.PassEntry.ID, PassEntry{}) if err != nil { writeErr(w, err) return } } - q := `select id,secret from apppass where secret = ?` passEntries := []PassEntry{} - err := p.db.Select(&passEntries, q, req.PassEntry.Secret) + err := p.store.Find(&passEntries, bh.Where("secret").Eq(req.PassEntry.Secret)) if err != nil { writeErr(w, err) return @@ -172,12 +173,8 @@ func (p *AdminPlugin) handleVars(w http.ResponseWriter, r *http.Request) { } func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) { - var configEntries []struct { - Key string `json:"key"` - Value string `json:"value"` - } - q := `select key, value from config` - err := p.db.Select(&configEntries, q) + configEntries := []config.Value{} + err := p.store.Find(&configEntries, &bh.Query{}) if err != nil { log.Error(). Err(err). diff --git a/plugins/babbler/babbler.go b/plugins/babbler/babbler.go index f96a756..45f9880 100644 --- a/plugins/babbler/babbler.go +++ b/plugins/babbler/babbler.go @@ -6,13 +6,14 @@ import ( "database/sql" "errors" "fmt" + bh "github.com/timshannon/bolthold" + "github.com/velour/catbase/plugins/remember" "math/rand" "regexp" "strings" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) @@ -25,74 +26,65 @@ var ( type BabblerPlugin struct { Bot bot.Bot - db *sqlx.DB + store *bh.Store WithGoRoutines bool handlers bot.HandlerTable } type Babbler struct { - BabblerId int64 `db:"id"` + BabblerId int64 `db:"id" boltholdid:"BabblerId"` Name string `db:"babbler"` } +func getBabbler(store *bh.Store, id int64) (*Babbler, error) { + res := &Babbler{} + err := store.Get(id, res) + return res, err +} + type BabblerWord struct { - WordId int64 `db:"id"` + WordId int64 `db:"id" boltholdid:"WordId"` Word string `db:"word"` } +func getWord(store *bh.Store, id int64) (*BabblerWord, error) { + res := &BabblerWord{} + err := store.Get(id, res) + return res, err +} + type BabblerNode struct { - NodeId int64 `db:"id"` + NodeId int64 `db:"id" boltholdid:"NodeId"` BabblerId int64 `db:"babblerId"` WordId int64 `db:"wordId"` Root int64 `db:"root"` RootFrequency int64 `db:"rootFrequency"` } +func getNode(store *bh.Store, id int64) (*BabblerNode, error) { + res := &BabblerNode{} + err := store.Get(id, res) + return res, err +} + type BabblerArc struct { - ArcId int64 `db:"id"` + ArcId int64 `db:"id" boltholdid:"ArcId"` FromNodeId int64 `db:"fromNodeId"` ToNodeId int64 `db:"toNodeId"` Frequency int64 `db:"frequency"` } +func getArc(store *bh.Store, id int64) (*BabblerArc, error) { + res := &BabblerArc{} + err := store.Get(id, res) + return res, err +} + func New(b bot.Bot) *BabblerPlugin { - if _, err := b.DB().Exec(`create table if not exists babblers ( - id integer primary key, - babbler string - );`); err != nil { - log.Fatal().Err(err) - } - - if _, err := b.DB().Exec(`create table if not exists babblerWords ( - id integer primary key, - word string - );`); err != nil { - log.Fatal().Err(err) - } - - if _, err := b.DB().Exec(`create table if not exists babblerNodes ( - id integer primary key, - babblerId integer, - wordId integer, - root integer, - rootFrequency integer - );`); err != nil { - log.Fatal().Err(err) - } - - if _, err := b.DB().Exec(`create table if not exists babblerArcs ( - id integer primary key, - fromNodeId integer, - toNodeId interger, - frequency integer - );`); err != nil { - log.Fatal().Err(err) - } - plugin := &BabblerPlugin{ Bot: b, - db: b.DB(), + store: b.Store(), WithGoRoutines: true, } @@ -195,24 +187,20 @@ func (p *BabblerPlugin) help(c bot.Connector, kind bot.Kind, msg msg.Message, ar } func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) { - res, err := p.db.Exec(`insert into babblers (babbler) values (?);`, name) - if err == nil { - id, err := res.LastInsertId() - if err != nil { - log.Error().Err(err) - return nil, err - } - return &Babbler{ - BabblerId: id, - Name: name, - }, nil + b := &Babbler{ + Name: name, } - return nil, err + err := p.store.Insert(bh.NextSequence(), b) + if err != nil { + log.Error().Err(err) + return nil, err + } + return b, err } func (p *BabblerPlugin) getBabbler(name string) (*Babbler, error) { var bblr Babbler - err := p.db.QueryRowx(`select * from babblers where babbler = ? LIMIT 1;`, name).StructScan(&bblr) + err := p.store.FindOne(&bblr, bh.Where("babbler").Eq(name)) if err != nil { if err == sql.ErrNoRows { log.Error().Msg("failed to find babbler") @@ -233,29 +221,9 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) { return nil, err } - rows, err := p.db.Queryx(fmt.Sprintf("select tidbit from factoid where fact like '%s quotes';", babbler.Name)) - if err != nil { - log.Error().Err(err) - return babbler, nil - } - defer rows.Close() - - tidbits := []string{} - for rows.Next() { - var tidbit string - err := rows.Scan(&tidbit) - - log.Debug().Str("tidbit", tidbit) - - if err != nil { - log.Error().Err(err) - return babbler, err - } - tidbits = append(tidbits, tidbit) - } - - for _, tidbit := range tidbits { - if err = p.addToMarkovChain(babbler, tidbit); err != nil { + quotes := remember.AllQuotesFrom(p.store, babbler.Name) + for _, q := range quotes { + if err = p.addToMarkovChain(babbler, q.Tidbit); err != nil { log.Error().Err(err) } } @@ -265,9 +233,9 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) { func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) { var w BabblerWord - err := p.db.QueryRowx(`select * from babblerWords where word = ? LIMIT 1;`, word).StructScan(&w) + err := p.store.FindOne(&w, bh.Where("word").Eq(word).Limit(1)) if err != nil { - if err == sql.ErrNoRows { + if err == bh.ErrNotFound { return nil, NEVER_SAID } return nil, err @@ -276,20 +244,13 @@ func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) { } func (p *BabblerPlugin) createNewWord(word string) (*BabblerWord, error) { - res, err := p.db.Exec(`insert into babblerWords (word) values (?);`, word) + w := &BabblerWord{Word: word} + err := p.store.Insert(bh.NextSequence(), w) if err != nil { log.Error().Err(err) return nil, err } - id, err := res.LastInsertId() - if err != nil { - log.Error().Err(err) - return nil, err - } - return &BabblerWord{ - WordId: id, - Word: word, - }, nil + return w, nil } func (p *BabblerPlugin) getOrCreateWord(word string) (*BabblerWord, error) { @@ -310,9 +271,9 @@ func (p *BabblerPlugin) getBabblerNode(babbler *Babbler, word string) (*BabblerN } var node BabblerNode - err = p.db.QueryRowx(`select * from babblerNodes where babblerId = ? and wordId = ? LIMIT 1;`, babbler.BabblerId, w.WordId).StructScan(&node) + err = p.store.FindOne(&node, bh.Where("babblerId").Eq(babbler.BabblerId).And("wordId").Eq(w.WordId)) if err != nil { - if err == sql.ErrNoRows { + if err == bh.ErrNotFound { return nil, NEVER_SAID } return nil, err @@ -327,24 +288,19 @@ func (p *BabblerPlugin) createBabblerNode(babbler *Babbler, word string) (*Babbl return nil, err } - res, err := p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?, ?, 0, 0)`, babbler.BabblerId, w.WordId) - if err != nil { - log.Error().Err(err) - return nil, err - } - - id, err := res.LastInsertId() - if err != nil { - log.Error().Err(err) - return nil, err - } - - return &BabblerNode{ - NodeId: id, + bn := &BabblerNode{ WordId: w.WordId, Root: 0, RootFrequency: 0, - }, nil + } + + err = p.store.Insert(bh.NextSequence(), bn) + if err != nil { + log.Error().Err(err) + return nil, err + } + + return bn, nil } func (p *BabblerPlugin) getOrCreateBabblerNode(babbler *Babbler, word string) (*BabblerNode, error) { @@ -361,7 +317,12 @@ func (p *BabblerPlugin) incrementRootWordFrequency(babbler *Babbler, word string log.Error().Err(err) return nil, err } - _, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + 1, root = 1 where id = ?;`, node.NodeId) + err = p.store.UpdateMatching(BabblerNode{}, bh.Where("id").Eq(node.NodeId), func(record interface{}) error { + r := record.(BabblerNode) + r.RootFrequency += 1 + r.Root = 1 + return p.store.Update(r.NodeId, r) + }) if err != nil { log.Error().Err(err) return nil, err @@ -372,9 +333,9 @@ func (p *BabblerPlugin) incrementRootWordFrequency(babbler *Babbler, word string func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerArc, error) { var arc BabblerArc - err := p.db.QueryRowx(`select * from babblerArcs where fromNodeId = ? and toNodeId = ?;`, fromNode.NodeId, toNode.NodeId).StructScan(&arc) + err := p.store.FindOne(&arc, bh.Where("fromNodeId").Eq(fromNode.NodeId).And("toNodeId").Eq(toNode.NodeId)) if err != nil { - if err == sql.ErrNoRows { + if err == bh.ErrNotFound { return nil, NEVER_SAID } return nil, err @@ -383,24 +344,32 @@ func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerAr } func (p *BabblerPlugin) incrementWordArc(fromNode, toNode *BabblerNode) (*BabblerArc, error) { - res, err := p.db.Exec(`update babblerArcs set frequency = frequency + 1 where fromNodeId = ? and toNodeId = ?;`, fromNode.NodeId, toNode.NodeId) + affectedRows := 0 + err := p.store.UpdateMatching(BabblerArc{}, + bh.Where("fromNodeId").Eq(fromNode.NodeId).And("toNodeId").Eq(toNode.NodeId), + func(record interface{}) error { + affectedRows++ + r := record.(BabblerArc) + r.Frequency += 1 + return p.store.Update(r.ArcId, r) + }) if err != nil { log.Error().Err(err) return nil, err } - affectedRows := int64(0) - if err == nil { - affectedRows, _ = res.RowsAffected() - } - if affectedRows == 0 { - res, err = p.db.Exec(`insert into babblerArcs (fromNodeId, toNodeId, frequency) values (?, ?, 1);`, fromNode.NodeId, toNode.NodeId) + p.store.Insert(bh.NextSequence(), BabblerArc{ + FromNodeId: fromNode.NodeId, + ToNodeId: toNode.NodeId, + Frequency: 1, + }) if err != nil { log.Error().Err(err) return nil, err } } + return p.getBabblerArc(fromNode, toNode) } @@ -444,25 +413,17 @@ func (p *BabblerPlugin) addToMarkovChain(babbler *Babbler, phrase string) error } func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *BabblerWord, error) { - rows, err := p.db.Queryx(`select * from babblerNodes where babblerId = ? and root = 1;`, babbler.BabblerId) + rootNodes := []*BabblerNode{} + err := p.store.Find(&rootNodes, bh.Where("babblerId").Eq(babbler.BabblerId).And("root").Eq(1)) if err != nil { log.Error().Err(err) return nil, nil, err } - defer rows.Close() - rootNodes := []*BabblerNode{} total := int64(0) - for rows.Next() { - var node BabblerNode - err = rows.StructScan(&node) - if err != nil { - log.Error().Err(err) - return nil, nil, err - } - rootNodes = append(rootNodes, &node) - total += node.RootFrequency + for _, n := range rootNodes { + total += n.RootFrequency } if len(rootNodes) == 0 { @@ -474,13 +435,12 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba for _, node := range rootNodes { total += node.RootFrequency if total >= which { - var w BabblerWord - err := p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) + w, err := getWord(p.store, node.WordId) if err != nil { log.Error().Err(err) return nil, nil, err } - return node, &w, nil + return node, w, nil } } @@ -489,24 +449,15 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba } func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode, *BabblerWord, error) { - rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, fromNode.NodeId) + arcs := []BabblerArc{} + err := p.store.Find(&arcs, bh.Where("fromNodeId").Eq(fromNode.NodeId)) if err != nil { log.Error().Err(err) return nil, nil, err } - defer rows.Close() - - arcs := []*BabblerArc{} total := int64(0) - for rows.Next() { - var arc BabblerArc - err = rows.StructScan(&arc) - if err != nil { - log.Error().Err(err) - return nil, nil, err - } - arcs = append(arcs, &arc) - total += arc.Frequency + for _, a := range arcs { + total += a.Frequency } if len(arcs) == 0 { @@ -520,20 +471,18 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode total += arc.Frequency if total >= which { - var node BabblerNode - err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.ToNodeId).StructScan(&node) + node, err := getNode(p.store, arc.ToNodeId) if err != nil { log.Error().Err(err) return nil, nil, err } - var w BabblerWord - err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) + w, err := getWord(p.store, node.WordId) if err != nil { log.Error().Err(err) return nil, nil, err } - return &node, &w, nil + return node, w, nil } } @@ -542,23 +491,15 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode } func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNode, *BabblerWord, bool, error) { - rows, err := p.db.Queryx(`select * from babblerArcs where toNodeId = ?;`, toNode.NodeId) + arcs := []*BabblerArc{} + err := p.store.Find(&arcs, bh.Where("toNodeId").Eq(toNode.NodeId)) if err != nil { log.Error().Err(err) return nil, nil, false, err } - defer rows.Close() - arcs := []*BabblerArc{} total := int64(0) - for rows.Next() { - var arc BabblerArc - err = rows.StructScan(&arc) - if err != nil { - log.Error().Err(err) - return nil, nil, false, err - } - arcs = append(arcs, &arc) + for _, arc := range arcs { total += arc.Frequency } @@ -575,24 +516,21 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo total = 0 for _, arc := range arcs { - total += arc.Frequency if total >= which { - var node BabblerNode - err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.FromNodeId).StructScan(&node) + node, err := getNode(p.store, arc.FromNodeId) if err != nil { log.Error().Err(err) return nil, nil, false, err } - var w BabblerWord - err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) + w, err := getWord(p.store, node.WordId) if err != nil { log.Error().Err(err) return nil, nil, false, err } - return &node, &w, false, nil + return node, w, false, nil } } log.Fatal().Msg("failed to find weighted previous word") @@ -686,51 +624,49 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa mapping := map[int64]*BabblerNode{} - rows, err := p.db.Queryx("select * from babblerNodes where babblerId = ?;", otherBabbler.BabblerId) + nodes := []*BabblerNode{} + err = p.store.Find(&nodes, bh.Where("babblerId").Eq(otherBabbler.BabblerId)) if err != nil { log.Error().Err(err) return err } - defer rows.Close() - - nodes := []*BabblerNode{} - - for rows.Next() { - var node BabblerNode - err = rows.StructScan(&node) - if err != nil { - log.Error().Err(err) - return err - } - nodes = append(nodes, &node) - } for _, node := range nodes { - var res sql.Result - if node.NodeId == otherNode.NodeId { node.WordId = intoNode.WordId } + affected := 0 if node.Root > 0 { - res, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + ?, root = 1 where babblerId = ? and wordId = ?;`, node.RootFrequency, intoBabbler.BabblerId, node.WordId) + err = p.store.UpdateMatching(BabblerNode{}, + bh.Where("babblerId").Eq(intoBabbler.BabblerId).And("wordId").Eq(node.WordId), + func(record interface{}) error { + affected++ + r := record.(BabblerNode) + r.RootFrequency += node.RootFrequency + r.Root = 1 + return p.store.Update(r.BabblerId, r) + }) if err != nil { log.Error().Err(err) } } else { - res, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + ? where babblerId = ? and wordId = ?;`, node.RootFrequency, intoBabbler.BabblerId, node.WordId) + err = p.store.UpdateMatching(BabblerNode{}, + bh.Where("babblerId").Eq(intoBabbler.BabblerId).And("wordId").Eq(node.WordId), + func(record interface{}) error { + affected++ + r := record.(BabblerNode) + r.RootFrequency += node.RootFrequency + return p.store.Update(r.BabblerId, r) + }) if err != nil { log.Error().Err(err) } } - rowsAffected := int64(-1) - if err == nil { - rowsAffected, _ = res.RowsAffected() - } - - if err != nil || rowsAffected == 0 { - res, err = p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?,?,?,?) ;`, intoBabbler.BabblerId, node.WordId, node.Root, node.RootFrequency) + if err != nil || affected == 0 { + node.BabblerId = intoBabbler.BabblerId + err = p.store.Insert(bh.NextSequence(), &node) if err != nil { log.Error().Err(err) return err @@ -738,7 +674,8 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa } var updatedNode BabblerNode - err = p.db.QueryRowx(`select * from babblerNodes where babblerId = ? and wordId = ? LIMIT 1;`, intoBabbler.BabblerId, node.WordId).StructScan(&updatedNode) + err = p.store.FindOne(&updatedNode, + bh.Where("babblerId").Eq(intoBabbler.BabblerId).And("wordId").Eq(node.WordId)) if err != nil { log.Error().Err(err) return err @@ -748,23 +685,11 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa } for oldNodeId, newNode := range mapping { - rows, err := p.db.Queryx("select * from babblerArcs where fromNodeId = ?;", oldNodeId) + arcs := []*BabblerArc{} + err = p.store.Find(&arcs, bh.Where("fromNodeId").Eq(oldNodeId)) if err != nil { return err } - defer rows.Close() - - arcs := []*BabblerArc{} - - for rows.Next() { - var arc BabblerArc - err = rows.StructScan(&arc) - if err != nil { - log.Error().Err(err) - return err - } - arcs = append(arcs, &arc) - } for _, arc := range arcs { _, err := p.incrementWordArc(newNode, mapping[arc.ToNodeId]) @@ -824,33 +749,21 @@ func (p *BabblerPlugin) babbleSeedSuffix(babblerName string, seed []string) (str func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error) { arcs := []*BabblerArc{} - rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, babblerNodeId) + err := p.store.Find(&arcs, bh.Where("fromNodeId").Eq(babblerNodeId)) if err != nil { log.Error().Err(err) return arcs, err } - defer rows.Close() - - for rows.Next() { - var arc BabblerArc - err = rows.StructScan(&arc) - if err != nil { - log.Error().Err(err) - return []*BabblerArc{}, err - } - arcs = append(arcs, &arc) - } return arcs, nil } func (p *BabblerPlugin) getBabblerNodeById(nodeId int64) (*BabblerNode, error) { - var node BabblerNode - err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, nodeId).StructScan(&node) + node, err := getNode(p.store, nodeId) if err != nil { log.Error().Err(err) return nil, err } - return &node, nil + return node, nil } func shuffle(a []*BabblerArc) { @@ -932,8 +845,7 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri log.Error().Err(err) return "", err } - var w BabblerWord - err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, cur.WordId).StructScan(&w) + w, err := getWord(p.store, cur.WordId) if err != nil { log.Error().Err(err) return "", err diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go index b1f47f5..b369b9d 100644 --- a/plugins/beers/beers.go +++ b/plugins/beers/beers.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + bh "github.com/timshannon/bolthold" "image" "image/png" "io/ioutil" @@ -21,7 +22,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" - "github.com/jmoiron/sqlx" "github.com/nfnt/resize" "github.com/rs/zerolog/log" @@ -31,24 +31,21 @@ import ( "github.com/velour/catbase/plugins/counter" ) -// This is a skeleton plugin to serve as an example and quick copy/paste for new plugins. - var cachedImages = map[string][]byte{} const DEFAULT_ITEM = "🍺" type BeersPlugin struct { - b bot.Bot - c *config.Config - db *sqlx.DB + b bot.Bot + c *config.Config + store *bh.Store untapdCache map[int]bool handlers bot.HandlerTable } type untappdUser struct { - id int64 - untappdUser string + untappdUser string `boltholdid:"untappdUser"` channel string lastCheckin int chanNick string @@ -56,19 +53,10 @@ type untappdUser struct { // New BeersPlugin creates a new BeersPlugin with the Plugin interface func New(b bot.Bot) *BeersPlugin { - if _, err := b.DB().Exec(`create table if not exists untappd ( - id integer primary key, - untappdUser string, - channel string, - lastCheckin integer, - chanNick string - );`); err != nil { - log.Fatal().Err(err) - } p := &BeersPlugin{ - b: b, - c: b.Config(), - db: b.DB(), + b: b, + c: b.Config(), + store: b.Store(), untapdCache: make(map[int]bool), } @@ -182,9 +170,7 @@ func (p *BeersPlugin) register() { Str("nick", u.chanNick). Msg("Creating Untappd user") - var count int - err := p.db.QueryRow(`select count(*) from untappd - where untappdUser = ?`, u.untappdUser).Scan(&count) + count, err := p.store.Count(untappdUser{}, bh.Where("untappdUser").Eq(u.untappdUser)) if err != nil { log.Error().Err(err).Msgf("Error registering untappd") } @@ -192,17 +178,7 @@ func (p *BeersPlugin) register() { p.b.Send(r.Conn, bot.Message, channel, "I'm already watching you.") return true } - _, err = p.db.Exec(`insert into untappd ( - untappdUser, - channel, - lastCheckin, - chanNick - ) values (?, ?, ?, ?);`, - u.untappdUser, - u.channel, - 0, - u.chanNick, - ) + err = p.store.Insert(u.untappdUser, u) if err != nil { log.Error().Err(err).Msgf("Error registering untappd") p.b.Send(r.Conn, bot.Message, channel, "I can't see.") @@ -240,15 +216,15 @@ func (p *BeersPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, return true } -func getUserBeers(db *sqlx.DB, user, id, itemName string) counter.Item { +func getUserBeers(store *bh.Store, user, id, itemName string) counter.Item { // TODO: really ought to have an ID here - booze, _ := counter.GetUserItem(db, user, id, itemName) + booze, _ := counter.GetUserItem(store, user, id, itemName) return booze } func (p *BeersPlugin) setBeers(r *bot.Request, user, id string, amount int) { itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) - ub := getUserBeers(p.db, user, id, itemName) + ub := getUserBeers(p.store, user, id, itemName) err := ub.Update(r, amount) if err != nil { log.Error().Err(err).Msgf("Error saving beers") @@ -257,7 +233,7 @@ func (p *BeersPlugin) setBeers(r *bot.Request, user, id string, amount int) { func (p *BeersPlugin) addBeers(r *bot.Request, user, id string, delta int) { itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) - ub := getUserBeers(p.db, user, id, itemName) + ub := getUserBeers(p.store, user, id, itemName) err := ub.UpdateDelta(r, delta) if err != nil { log.Error().Err(err).Msgf("Error saving beers") @@ -266,7 +242,7 @@ func (p *BeersPlugin) addBeers(r *bot.Request, user, id string, delta int) { func (p *BeersPlugin) getBeers(user, id string) int { itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) - ub := getUserBeers(p.db, user, id, itemName) + ub := getUserBeers(p.store, user, id, itemName) return ub.Count } @@ -401,17 +377,13 @@ func (p *BeersPlugin) checkUntappd(c bot.Connector, channel string) { } userMap := make(map[string]untappdUser) - rows, err := p.db.Query(`select id, untappdUser, channel, lastCheckin, chanNick from untappd;`) + users := []untappdUser{} + err := p.store.Find(&users, &bh.Query{}) if err != nil { log.Error().Err(err).Msg("Error getting untappd users") return } - for rows.Next() { - u := untappdUser{} - err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick) - if err != nil { - log.Fatal().Err(err) - } + for _, u := range users { userMap[u.untappdUser] = u if u.chanNick == "" { log.Fatal().Msg("Empty chanNick for no good reason.") @@ -497,9 +469,8 @@ func (p *BeersPlugin) sendCheckin(c bot.Connector, channel string, user untappdU args = append([]interface{}{channel, msg}, args...) user.lastCheckin = checkin.Checkin_id - _, err := p.db.Exec(`update untappd set - lastCheckin = ? - where id = ?`, user.lastCheckin, user.id) + + err := p.store.Update(user.untappdUser, user) if err != nil { log.Error().Err(err).Msg("UPDATE ERROR!") } diff --git a/plugins/countdown/countdown.go b/plugins/countdown/countdown.go index 57865d0..c2f7a30 100644 --- a/plugins/countdown/countdown.go +++ b/plugins/countdown/countdown.go @@ -3,9 +3,9 @@ package countdown import ( "fmt" + bh "github.com/timshannon/bolthold" "time" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" @@ -16,16 +16,16 @@ var nextYear = time.Date(time.Now().Year()+1, time.January, 1, 0, 0, 0, 1, time. var thisYear = time.Now().Year() type CountdownPlugin struct { - b bot.Bot - db *sqlx.DB - c *config.Config + b bot.Bot + store *bh.Store + c *config.Config } func New(b bot.Bot) *CountdownPlugin { p := &CountdownPlugin{ - b: b, - db: b.DB(), - c: b.Config(), + b: b, + store: b.Store(), + c: b.Config(), } p.scheduleNYE(p.newYearRollover) @@ -48,27 +48,28 @@ func (p *CountdownPlugin) scheduleNYE(f func()) { func (p *CountdownPlugin) newYearRollover() { log.Debug().Msgf("Happy new year!") - tx, err := p.db.Beginx() - if err != nil { - logError(err, "error getting transaction") - return - } - q := fmt.Sprintf(`create table counter_%d as select * from counter`, thisYear) - _, err = tx.Exec(q) - if err != nil { - logError(err, "error running insert into") - logError(tx.Rollback(), "error with insert rollback") - return - } - q = `delete from counter` - _, err = tx.Exec(q) - if err != nil { - logError(err, "error running delete") - logError(tx.Rollback(), "error with delete rollback") - return - } - err = tx.Commit() - logError(err, "error committing transaction") + // todo: something about rolling over + //tx, err := p.db.Beginx() + //if err != nil { + // logError(err, "error getting transaction") + // return + //} + //q := fmt.Sprintf(`create table counter_%d as select * from counter`, thisYear) + //_, err = tx.Exec(q) + //if err != nil { + // logError(err, "error running insert into") + // logError(tx.Rollback(), "error with insert rollback") + // return + //} + //q = `delete from counter` + //_, err = tx.Exec(q) + //if err != nil { + // logError(err, "error running delete") + // logError(tx.Rollback(), "error with delete rollback") + // return + //} + //err = tx.Commit() + //logError(err, "error committing transaction") } func logError(err error, msg string) { diff --git a/plugins/counter/api.go b/plugins/counter/api.go index 4c78c1d..75c4444 100644 --- a/plugins/counter/api.go +++ b/plugins/counter/api.go @@ -59,7 +59,7 @@ func (p *CounterPlugin) mkIncrementAPI(delta int) func(w http.ResponseWriter, r return } - item, err := GetUserItem(p.db, userName, u.ID, itemName) + item, err := GetUserItem(p.store, userName, u.ID, itemName) if err != nil { log.Error().Err(err).Msg("error finding item") w.WriteHeader(400) @@ -136,7 +136,7 @@ func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) return } nick, id := p.resolveUser(bot.Request{Conn: p.b.DefaultConnector()}, info.User) - item, err := GetUserItem(p.db, nick, id, info.Thing) + item, err := GetUserItem(p.store, nick, id, info.Thing) if err != nil { log.Error(). Err(err). @@ -158,7 +158,7 @@ func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) } } - all, err := GetAllItems(p.db) + all, err := GetAllItems(p.store) if err != nil { w.WriteHeader(500) fmt.Fprint(w, err) diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 1cf4e55..5a314cb 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -3,6 +3,7 @@ package counter import ( "database/sql" "fmt" + bh "github.com/timshannon/bolthold" "math/rand" "regexp" "strconv" @@ -11,8 +12,6 @@ import ( "github.com/rs/zerolog/log" "github.com/velour/catbase/config" - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) @@ -20,13 +19,13 @@ import ( // This is a counter plugin to count arbitrary things. type CounterPlugin struct { - b bot.Bot - db *sqlx.DB - cfg *config.Config + b bot.Bot + store *bh.Store + cfg *config.Config } type Item struct { - *sqlx.DB + *bh.Store ID int64 Nick string @@ -36,7 +35,7 @@ type Item struct { } type alias struct { - *sqlx.DB + *bh.Store ID int64 Item string @@ -44,116 +43,97 @@ type alias struct { } // GetItems returns all counters -func GetAllItems(db *sqlx.DB) ([]Item, error) { +func GetAllItems(store *bh.Store) ([]Item, error) { var items []Item - err := db.Select(&items, `select * from counter`) + err := store.Find(&items, &bh.Query{}) if err != nil { return nil, err } // Don't forget to embed the db into all of that shiz for i := range items { - items[i].DB = db + items[i].Store = store } return items, nil } // GetItems returns all counters for a subject -func GetItems(db *sqlx.DB, nick, id string) ([]Item, error) { +func GetItems(store *bh.Store, nick, id string) ([]Item, error) { var items []Item var err error + q := &bh.Query{} if id != "" { - err = db.Select(&items, `select * from counter where userid = ?`, id) + q = bh.Where("userid").Eq(id) } else { - err = db.Select(&items, `select * from counter where nick = ?`, nick) + q = bh.Where("nick").Eq(nick) } - if err != nil { + if err = store.Find(&items, q); err != nil { return nil, err } // Don't forget to embed the db into all of that shiz for i := range items { - items[i].DB = db + items[i].Store = store } return items, nil } -func LeaderAll(db *sqlx.DB) ([]Item, error) { - s := `select id,item,nick,count from (select id,item,nick,count,max(abs(count)) from counter group by item having count(nick) > 1 and max(abs(count)) > 1) order by count desc` +func LeaderAll(store *bh.Store) ([]Item, error) { + //s := `select id,item,nick,count from (select id,item,nick,count,max(abs(count)) from counter group by item having count(nick) > 1 and max(abs(count)) > 1) order by count desc` + // todo: translate that query var items []Item - err := db.Select(&items, s) + err := store.Find(&items, &bh.Query{}) if err != nil { log.Error().Msgf("Error querying leaderboard: %s", err) return nil, err } for i := range items { - items[i].DB = db + items[i].Store = store } return items, nil } -func Leader(db *sqlx.DB, itemName string) ([]Item, error) { +func Leader(store *bh.Store, itemName string) ([]Item, error) { itemName = strings.ToLower(itemName) - s := `select * from counter where item=? order by count desc` + //s := `select * from counter where item=? order by count desc` + // todo: remove that when we verify this works var items []Item - err := db.Select(&items, s, itemName) + err := store.Find(&items, bh.Where("item").Eq(itemName).SortBy("count").Reverse()) if err != nil { return nil, err } for i := range items { - items[i].DB = db + items[i].Store = store } return items, nil } -func RmAlias(db *sqlx.DB, aliasName string) error { - q := `delete from counter_alias where item = ?` - tx, err := db.Beginx() - if err != nil { - return err - } - _, err = tx.Exec(q, aliasName) - if err != nil { - if err := tx.Rollback(); err != nil { - return err - } - return err - } - err = tx.Commit() - return err +func RmAlias(store *bh.Store, aliasName string) error { + return store.Delete(alias{}, aliasName) } -func MkAlias(db *sqlx.DB, aliasName, pointsTo string) (*alias, error) { +func MkAlias(store *bh.Store, aliasName, pointsTo string) (*alias, error) { aliasName = strings.ToLower(aliasName) pointsTo = strings.ToLower(pointsTo) - res, err := db.Exec(`insert into counter_alias (item, points_to) values (?, ?)`, - aliasName, pointsTo) - if err != nil { - _, err := db.Exec(`update counter_alias set points_to=? where item=?`, pointsTo, aliasName) - if err != nil { - return nil, err - } - var a alias - if err := db.Get(&a, `select * from counter_alias where item=?`, aliasName); err != nil { - return nil, err - } - return &a, nil + alias := &alias{ + Store: store, + Item: aliasName, + PointsTo: pointsTo, } - id, _ := res.LastInsertId() - - return &alias{db, id, aliasName, pointsTo}, nil + err := store.Insert(bh.NextSequence(), alias) + return alias, err } // GetUserItem returns a specific counter for all subjects -func GetItem(db *sqlx.DB, itemName string) ([]Item, error) { +func GetItem(store *bh.Store, itemName string) ([]Item, error) { itemName = trimUnicode(itemName) var items []Item var a alias - if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil { + if err := store.FindOne(&a, bh.Where("item").Eq(itemName)); err == nil { itemName = a.PointsTo } else { log.Error().Err(err).Interface("alias", a) } - err := db.Select(&items, `select * from counter where item= ?`, itemName) + err := store.Find(&items, bh.Where("item").Eq(itemName)) if err != nil { return nil, err } @@ -162,37 +142,30 @@ func GetItem(db *sqlx.DB, itemName string) ([]Item, error) { Interface("items", items). Msg("got item") for _, i := range items { - i.DB = db + i.Store = store } return items, nil } // GetUserItem returns a specific counter for a subject -func GetUserItem(db *sqlx.DB, nick, id, itemName string) (Item, error) { +func GetUserItem(store *bh.Store, nick, id, itemName string) (Item, error) { itemName = trimUnicode(itemName) var item Item - item.DB = db + item.Store = store var a alias - if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil { + if err := store.FindOne(&a, bh.Where("item").Eq(itemName)); err == nil { itemName = a.PointsTo } else { log.Error().Err(err).Interface("alias", a) } var err error + q := bh.Where("nick").Eq(nick).And("item").Eq(itemName) if id != "" { - err = db.Get(&item, `select * from counter where userid = ? and item= ?`, id, itemName) - } else { - err = db.Get(&item, `select * from counter where nick = ? and item= ?`, nick, itemName) + q = bh.Where("userid").Eq(id).And("item").Eq(itemName) } - switch err { - case sql.ErrNoRows: - item.ID = -1 - item.Nick = nick - item.Item = itemName - item.UserID = id - case nil: - default: + err = store.FindOne(&item, q) + if err != nil { return Item{}, err } log.Debug(). @@ -207,14 +180,7 @@ func GetUserItem(db *sqlx.DB, nick, id, itemName string) (Item, error) { // GetUserItem returns a specific counter for a subject // Create saves a counter func (i *Item) Create() error { - res, err := i.Exec(`insert into counter (nick, item, count, userid) values (?, ?, ?, ?);`, - i.Nick, i.Item, i.Count, i.UserID) - if err != nil { - return err - } - id, _ := res.LastInsertId() - // hackhackhack? - i.ID = id + err := i.Store.Insert(bh.NextSequence(), i) return err } @@ -232,7 +198,7 @@ func (i *Item) Update(r *bot.Request, value int) error { Interface("i", i). Int("value", value). Msg("Updating item") - _, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID) + err := i.Store.Update(i.ID, i) if err == nil { sendUpdate(r, i.Nick, i.Item, i.Count) } @@ -248,82 +214,49 @@ func (i *Item) UpdateDelta(r *bot.Request, delta int) error { // Delete removes a counter from the database func (i *Item) Delete() error { - _, err := i.Exec(`delete from counter where id = ?`, i.ID) + err := i.Store.Delete(i.ID, Item{}) i.ID = -1 return err } func (p *CounterPlugin) migrate(r bot.Request) bool { - db := p.db - - nicks := []string{} - err := db.Select(&nicks, `select distinct nick from counter where userid is null`) - if err != nil { - log.Error().Err(err).Msg("could not get nick list") - return false - } - - log.Debug().Msgf("Migrating %d nicks to IDs", len(nicks)) - - tx := db.MustBegin() - - for _, nick := range nicks { - user, err := r.Conn.Profile(nick) - if err != nil { - continue - } - if _, err = tx.Exec(`update counter set userid=? where nick=?`, user.ID, nick); err != nil { - log.Error().Err(err).Msg("Could not migrate users") - continue - } - } - - if err := tx.Commit(); err != nil { - log.Error().Err(err).Msg("Could not migrate users") - } + // todo: probably don't need this anymore + //db := p.db + // + //nicks := []string{} + //err := db.Select(&nicks, `select distinct nick from counter where userid is null`) + //if err != nil { + // log.Error().Err(err).Msg("could not get nick list") + // return false + //} + // + //log.Debug().Msgf("Migrating %d nicks to IDs", len(nicks)) + // + //tx := db.MustBegin() + // + //for _, nick := range nicks { + // user, err := r.Conn.Profile(nick) + // if err != nil { + // continue + // } + // if _, err = tx.Exec(`update counter set userid=? where nick=?`, user.ID, nick); err != nil { + // log.Error().Err(err).Msg("Could not migrate users") + // continue + // } + //} + // + //if err := tx.Commit(); err != nil { + // log.Error().Err(err).Msg("Could not migrate users") + //} return false } -func setupDB(b bot.Bot) error { - db := b.DB() - tx := db.MustBegin() - db.MustExec(`create table if not exists counter ( - id integer primary key, - nick string, - item string, - count integer - );`) - db.MustExec(`create table if not exists counter_alias ( - id integer PRIMARY KEY AUTOINCREMENT, - item string NOT NULL UNIQUE, - points_to string NOT NULL - );`) - tx.Commit() - - tx = db.MustBegin() - count := 0 - err := tx.Get(&count, `SELECT count(*) FROM pragma_table_info('counter') where name='userid'`) - if err != nil { - return err - } - if count == 0 { - tx.MustExec(`alter table counter add column userid string`) - } - tx.Commit() - - return nil -} - // NewCounterPlugin creates a new CounterPlugin with the Plugin interface func New(b bot.Bot) *CounterPlugin { - if err := setupDB(b); err != nil { - panic(err) - } - cp := &CounterPlugin{ - b: b, - db: b.DB(), - cfg: b.Config(), + b: b, + store: b.Store(), + cfg: b.Config(), } b.RegisterRegex(cp, bot.Startup, regexp.MustCompile(`.*`), cp.migrate) @@ -376,7 +309,7 @@ func (p *CounterPlugin) mkAliasCmd(r bot.Request) bool { p.b.Send(r.Conn, bot.Message, fmt.Sprintf("You must provide all fields for an alias: %s", mkAliasRegex)) return true } - if _, err := MkAlias(p.db, what, to); err != nil { + if _, err := MkAlias(p.store, what, to); err != nil { log.Error().Err(err).Msg("Could not mkalias") p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "We're gonna need too much db space to make an alias for your mom.") return true @@ -392,7 +325,7 @@ func (p *CounterPlugin) rmAliasCmd(r bot.Request) bool { p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "You must specify an alias to remove.") return true } - if err := RmAlias(p.db, what); err != nil { + if err := RmAlias(p.store, what); err != nil { log.Error().Err(err).Msg("could not RmAlias") p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "`sudo rm your mom` => Nope, she's staying with me.") return true @@ -407,10 +340,10 @@ func (p *CounterPlugin) leaderboardCmd(r bot.Request) bool { what := r.Values["what"] if what == "" { - cmd = func() ([]Item, error) { return LeaderAll(p.db) } + cmd = func() ([]Item, error) { return LeaderAll(p.store) } } else { itNameTxt = fmt.Sprintf(" for %s", what) - cmd = func() ([]Item, error) { return Leader(p.db, what) } + cmd = func() ([]Item, error) { return Leader(p.store, what) } } its, err := cmd() @@ -438,7 +371,7 @@ func (p *CounterPlugin) resetCmd(r bot.Request) bool { nick, id := p.resolveUser(r, "") channel := r.Msg.Channel - items, err := GetItems(p.db, nick, id) + items, err := GetItems(p.store, nick, id) if err != nil { log.Error(). Err(err). @@ -472,7 +405,7 @@ func (p *CounterPlugin) inspectCmd(r bot.Request) bool { Str("id", id). Msg("Getting counter") // pull all of the items associated with "subject" - items, err := GetItems(p.db, nick, id) + items, err := GetItems(p.store, nick, id) if err != nil { log.Error(). Err(err). @@ -513,7 +446,7 @@ func (p *CounterPlugin) clearCmd(r bot.Request) bool { channel := r.Msg.Channel c := r.Conn - it, err := GetUserItem(p.db, nick, id, itemName) + it, err := GetUserItem(p.store, nick, id, itemName) if err != nil { log.Error(). Err(err). @@ -551,7 +484,7 @@ func (p *CounterPlugin) countCmd(r bot.Request) bool { } var item Item - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) switch { case err == sql.ErrNoRows: p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("I don't think %s has any %s.", @@ -581,7 +514,7 @@ func (p *CounterPlugin) incrementCmd(r bot.Request) bool { itemName := r.Values["thing"] channel := r.Msg.Channel // ++ those fuckers - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) if err != nil { log.Error(). Err(err). @@ -606,7 +539,7 @@ func (p *CounterPlugin) decrementCmd(r bot.Request) bool { itemName := r.Values["thing"] channel := r.Msg.Channel // -- those fuckers - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) if err != nil { log.Error(). Err(err). @@ -628,7 +561,7 @@ func (p *CounterPlugin) addToCmd(r bot.Request) bool { itemName := r.Values["thing"] channel := r.Msg.Channel // += those fuckers - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) if err != nil { log.Error(). Err(err). @@ -652,7 +585,7 @@ func (p *CounterPlugin) removeFromCmd(r bot.Request) bool { itemName := r.Values["thing"] channel := r.Msg.Channel // -= those fuckers - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) if err != nil { log.Error(). Err(err). @@ -693,7 +626,7 @@ func (p *CounterPlugin) teaMatchCmd(r bot.Request) bool { itemName := strings.ToLower(submatches[1]) // We will specifically allow :tea: to keep compatability - item, err := GetUserItem(p.db, nick, id, itemName) + item, err := GetUserItem(p.store, nick, id, itemName) if err != nil || (item.Count == 0 && item.Item != ":tea:") { log.Error(). Err(err). diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go index c89af1a..1fe33d8 100644 --- a/plugins/fact/factoid.go +++ b/plugins/fact/factoid.go @@ -3,10 +3,10 @@ package fact import ( - "database/sql" "embed" "encoding/json" "fmt" + bh "github.com/timshannon/bolthold" "html/template" "math/rand" "net/http" @@ -18,8 +18,6 @@ import ( "github.com/go-chi/chi/v5" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) @@ -32,7 +30,7 @@ var embeddedFS embed.FS // Factoid stores info about our factoid for lookup and later interaction type Factoid struct { - ID sql.NullInt64 + ID int64 `boltholdid:"ID"` Fact string Tidbit string Verb string @@ -47,47 +45,50 @@ type alias struct { Next string } -func (a *alias) resolve(db *sqlx.DB) (*Factoid, error) { +func (a *alias) resolve(store *bh.Store) (*Factoid, error) { // perform db query to fill the To field - q := `select fact, next from factoid_alias where fact=?` + // todo: remove this query + //q := `select fact, next from factoid_alias where fact=?` var next alias - err := db.Get(&next, q, a.Next) + err := store.FindOne(&next, bh.Where("fact").Eq(a.Next)) if err != nil { // we hit the end of the chain, get a factoid named Next - fact, err := GetSingleFact(db, a.Next) + fact, err := GetSingleFact(store, a.Next) if err != nil { err := fmt.Errorf("Error resolvig alias %v: %v", a, err) return nil, err } return fact, nil } - return next.resolve(db) + return next.resolve(store) } -func findAlias(db *sqlx.DB, fact string) (bool, *Factoid) { - q := `select * from factoid_alias where fact=?` +func findAlias(store *bh.Store, fact string) (bool, *Factoid) { + // todo: remove this query + //q := `select * from factoid_alias where fact=?` var a alias - err := db.Get(&a, q, fact) + err := store.FindOne(&a, bh.Where("fact").Eq(fact)) if err != nil { return false, nil } - f, err := a.resolve(db) + f, err := a.resolve(store) return err == nil, f } -func (a *alias) save(db *sqlx.DB) error { - q := `select * from factoid_alias where fact=?` +func (a *alias) save(store *bh.Store) error { + //q := `select * from factoid_alias where fact=?` var offender alias - err := db.Get(&offender, q, a.Next) + err := store.FindOne(&offender, bh.Where("fact").Eq(a.Next)) if err == nil { return fmt.Errorf("DANGER: an opposite alias already exists") } - _, err = a.resolve(db) + _, err = a.resolve(store) if err != nil { return fmt.Errorf("there is no fact at that destination") } - q = `insert or replace into factoid_alias (fact, next) values (?, ?)` - _, err = db.Exec(q, a.Fact, a.Next) + err = store.Upsert(a.Fact, a) + //q = `insert or replace into factoid_alias (fact, next) values (?, ?)` + // todo: remove query if err != nil { return err } @@ -98,168 +99,54 @@ func aliasFromStrings(from, to string) *alias { return &alias{from, to} } -func (f *Factoid) Save(db *sqlx.DB) error { +func (f *Factoid) Save(store *bh.Store) error { var err error - if f.ID.Valid { - // update - _, err = db.Exec(`update factoid set - fact=?, - tidbit=?, - verb=?, - owner=?, - accessed=?, - count=? - where id=?`, - f.Fact, - f.Tidbit, - f.Verb, - f.Owner, - f.Accessed.Unix(), - f.Count, - f.ID.Int64) + if f.ID != 0 { + f.Accessed = time.Now() + err = store.Update(f.ID, f) } else { f.Created = time.Now() f.Accessed = time.Now() - // insert - res, err := db.Exec(`insert into factoid ( - fact, - tidbit, - verb, - owner, - created, - accessed, - count - ) values (?, ?, ?, ?, ?, ?, ?);`, - f.Fact, - f.Tidbit, - f.Verb, - f.Owner, - f.Created.Unix(), - f.Accessed.Unix(), - f.Count, - ) - if err != nil { - return err - } - id, err := res.LastInsertId() - // hackhackhack? - f.ID.Int64 = id - f.ID.Valid = true + err = store.Insert(bh.NextSequence(), f) } return err } -func (f *Factoid) delete(db *sqlx.DB) error { +func (f *Factoid) delete(store *bh.Store) error { var err error - if f.ID.Valid { - _, err = db.Exec(`delete from factoid where id=?`, f.ID) + if f.ID != 0 { + err = store.Delete(f.ID, Factoid{}) } - f.ID.Valid = false + f.ID = 0 return err } -func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*Factoid, error) { +func getFacts(store *bh.Store, fact string, tidbit string) ([]*Factoid, error) { var fs []*Factoid - query := `select - id, - fact, - tidbit, - verb, - owner, - created, - accessed, - count - from factoid - where fact like ? - and tidbit like ?;` - rows, err := db.Query(query, - "%"+fact+"%", "%"+tidbit+"%") + err := store.Find(&fs, bh.Where("fact").Contains(fact).And("tidbit").Contains(tidbit)) if err != nil { log.Error().Err(err).Msg("Error regexping for facts") return nil, err } - for rows.Next() { - var f Factoid - var tmpCreated int64 - var tmpAccessed int64 - err := rows.Scan( - &f.ID, - &f.Fact, - &f.Tidbit, - &f.Verb, - &f.Owner, - &tmpCreated, - &tmpAccessed, - &f.Count, - ) - if err != nil { - return nil, err - } - f.Created = time.Unix(tmpCreated, 0) - f.Accessed = time.Unix(tmpAccessed, 0) - fs = append(fs, &f) - } return fs, err } -func GetSingle(db *sqlx.DB) (*Factoid, error) { - var f Factoid - var tmpCreated int64 - var tmpAccessed int64 - err := db.QueryRow(`select - id, - fact, - tidbit, - verb, - owner, - created, - accessed, - count - from factoid - order by random() limit 1;`).Scan( - &f.ID, - &f.Fact, - &f.Tidbit, - &f.Verb, - &f.Owner, - &tmpCreated, - &tmpAccessed, - &f.Count, - ) - f.Created = time.Unix(tmpCreated, 0) - f.Accessed = time.Unix(tmpAccessed, 0) - return &f, err +func GetSingle(store *bh.Store) (*Factoid, error) { + var allMatching []Factoid + if err := store.Find(&allMatching, &bh.Query{}); err != nil { + return nil, err + } + f := allMatching[rand.Intn(len(allMatching))] + return &f, nil } -func GetSingleFact(db *sqlx.DB, fact string) (*Factoid, error) { - var f Factoid - var tmpCreated int64 - var tmpAccessed int64 - err := db.QueryRow(`select - id, - fact, - tidbit, - verb, - owner, - created, - accessed, - count - from factoid - where fact like ? - order by random() limit 1;`, - fact).Scan( - &f.ID, - &f.Fact, - &f.Tidbit, - &f.Verb, - &f.Owner, - &tmpCreated, - &tmpAccessed, - &f.Count, - ) - f.Created = time.Unix(tmpCreated, 0) - f.Accessed = time.Unix(tmpAccessed, 0) - return &f, err +func GetSingleFact(store *bh.Store, fact string) (*Factoid, error) { + var allMatching []Factoid + if err := store.Find(&allMatching, bh.Where("fact").Contains(fact)); err != nil { + return nil, err + } + f := allMatching[rand.Intn(len(allMatching))] + return &f, nil } // Factoid provides the necessary plugin-wide needs @@ -267,7 +154,7 @@ type FactoidPlugin struct { Bot bot.Bot NotFound []string LastFact *Factoid - db *sqlx.DB + store *bh.Store handlers bot.HandlerTable } @@ -283,32 +170,11 @@ func New(botInst bot.Bot) *FactoidPlugin { "NOPE! NOPE! NOPE!", "One time, I learned how to jump rope.", }, - db: botInst.DB(), + store: botInst.Store(), } c := botInst.DefaultConnector() - if _, err := p.db.Exec(`create table if not exists factoid ( - id integer primary key, - fact string, - tidbit string, - verb string, - owner string, - created integer, - accessed integer, - count integer - );`); err != nil { - log.Fatal().Err(err) - } - - if _, err := p.db.Exec(`create table if not exists factoid_alias ( - fact string, - next string, - primary key (fact, next) - );`); err != nil { - log.Fatal().Err(err) - } - for _, channel := range botInst.Config().GetArray("channels", []string{}) { go p.factTimer(c, channel) @@ -366,14 +232,11 @@ func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string } } - var count sql.NullInt64 - err := p.db.QueryRow(`select count(*) from factoid - where fact=? and verb=? and tidbit=?`, - fact, verb, tidbit).Scan(&count) + count, err := p.store.Count(Factoid{}, bh.Where("fact").Eq(fact).And("verb").Eq(verb).And("tidbit").Eq(tidbit)) if err != nil { log.Error().Err(err).Msg("Error counting facts") return fmt.Errorf("What?") - } else if count.Valid && count.Int64 != 0 { + } else if count != 0 { log.Debug().Msg("User tried to relearn a fact.") return fmt.Errorf("Look, I already know that.") } @@ -388,7 +251,7 @@ func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string Count: 0, } p.LastFact = &n - err = n.Save(p.db) + err = n.Save(p.store) if err != nil { log.Error().Err(err).Msg("Error inserting fact") return fmt.Errorf("My brain is overheating.") @@ -401,9 +264,9 @@ func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string func (p *FactoidPlugin) findTrigger(fact string) (bool, *Factoid) { fact = strings.ToLower(fact) // TODO: make sure this needs to be lowered here - f, err := GetSingleFact(p.db, fact) + f, err := GetSingleFact(p.store, fact) if err != nil { - return findAlias(p.db, fact) + return findAlias(p.store, fact) } return true, f } @@ -437,7 +300,7 @@ func (p *FactoidPlugin) sayFact(c bot.Connector, message msg.Message, fact Facto // update fact tracking fact.Accessed = time.Now() fact.Count += 1 - err := fact.Save(p.db) + err := fact.Save(p.store) if err != nil { log.Error(). Interface("fact", fact). @@ -506,7 +369,7 @@ func (p *FactoidPlugin) tellThemWhatThatWas(c bot.Connector, message msg.Message msg = "Nope." } else { msg = fmt.Sprintf("That was (#%d) '%s <%s> %s'", - fact.ID.Int64, fact.Fact, fact.Verb, fact.Tidbit) + fact.ID, fact.Fact, fact.Verb, fact.Tidbit) } p.Bot.Send(c, bot.Message, message.Channel, msg) return true @@ -564,14 +427,14 @@ func (p *FactoidPlugin) forgetLastFact(c bot.Connector, message msg.Message) boo return true } - err := p.LastFact.delete(p.db) + err := p.LastFact.delete(p.store) if err != nil { log.Error(). Err(err). Interface("LastFact", p.LastFact). Msg("Error removing fact") } - fmt.Printf("Forgot #%d: %s %s %s\n", p.LastFact.ID.Int64, p.LastFact.Fact, + fmt.Printf("Forgot #%d: %s %s %s\n", p.LastFact.ID, p.LastFact.Fact, p.LastFact.Verb, p.LastFact.Tidbit) p.Bot.Send(c, bot.Action, message.Channel, "hits himself over the head with a skillet") p.LastFact = nil @@ -603,7 +466,7 @@ func (p *FactoidPlugin) changeFact(c bot.Connector, message msg.Message) bool { replace := parts[2] // replacement - result, err := getFacts(p.db, trigger, parts[1]) + result, err := getFacts(p.store, trigger, parts[1]) if err != nil { log.Error(). Err(err). @@ -628,11 +491,11 @@ func (p *FactoidPlugin) changeFact(c bot.Connector, message msg.Message) bool { fact.Tidbit = reg.ReplaceAllString(fact.Tidbit, replace) fact.Count += 1 fact.Accessed = time.Now() - fact.Save(p.db) + fact.Save(p.store) } } else if len(parts) == 3 { // search for a factoid and print it - result, err := getFacts(p.db, trigger, parts[1]) + result, err := getFacts(p.store, trigger, parts[1]) if err != nil { log.Error(). Err(err). @@ -684,7 +547,7 @@ func (p *FactoidPlugin) register() { to := r.Values["to"] log.Debug().Msgf("alias: %+v", r) a := aliasFromStrings(from, to) - if err := a.save(p.db); err != nil { + if err := a.save(p.store); err != nil { p.Bot.Send(r.Conn, bot.Message, r.Msg.Channel, err.Error()) } else { p.Bot.Send(r.Conn, bot.Action, r.Msg.Channel, "learns a new synonym") @@ -748,7 +611,7 @@ func (p *FactoidPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message // Pull a fact at random from the database func (p *FactoidPlugin) randomFact() *Factoid { - f, err := GetSingle(p.db) + f, err := GetSingle(p.store) if err != nil { fmt.Println("Error getting a fact: ", err) return nil @@ -838,7 +701,7 @@ func (p *FactoidPlugin) serveAPI(w http.ResponseWriter, r *http.Request) { return } - entries, err := getFacts(p.db, info.Query, "") + entries, err := getFacts(p.store, info.Query, "") if err != nil { w.WriteHeader(500) fmt.Fprint(w, err) diff --git a/plugins/first/first.go b/plugins/first/first.go index 872c882..d17292a 100644 --- a/plugins/first/first.go +++ b/plugins/first/first.go @@ -3,13 +3,12 @@ package first import ( - "database/sql" "fmt" + bh "github.com/timshannon/bolthold" "regexp" "strings" "time" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/config" @@ -22,7 +21,7 @@ import ( type FirstPlugin struct { bot bot.Bot config *config.Config - db *sqlx.DB + store *bh.Store handlers bot.HandlerTable enabled bool } @@ -38,59 +37,23 @@ type FirstEntry struct { } // Insert or update the first entry -func (fe *FirstEntry) save(db *sqlx.DB) error { - if _, err := db.Exec(`insert into first (day, time, channel, body, nick) - values (?, ?, ?, ?, ?)`, - fe.day.Unix(), - fe.time.Unix(), - fe.channel, - fe.body, - fe.nick, - ); err != nil { - return err - } - return nil +func (fe *FirstEntry) save(store *bh.Store) error { + return store.Insert(bh.NextSequence(), fe) } -func (fe *FirstEntry) delete(db *sqlx.DB) error { - tx, err := db.Beginx() - if err != nil { - return err - } - _, err = tx.Exec(`delete from first where id=?`, fe.id) - if err != nil { - return err - } - err = tx.Commit() - if err != nil { - return err - } - return nil +func (fe *FirstEntry) delete(store *bh.Store) error { + return store.Delete(fe.id, FirstEntry{}) } // NewFirstPlugin creates a new FirstPlugin with the Plugin interface func New(b bot.Bot) *FirstPlugin { - _, err := b.DB().Exec(`create table if not exists first ( - id integer primary key, - day integer, - time integer, - channel string, - body string, - nick string - );`) - if err != nil { - log.Fatal(). - Err(err). - Msg("Could not create first table") - } - log.Info().Msgf("First plugin initialized with day: %s", Midnight(time.Now())) fp := &FirstPlugin{ bot: b, config: b.Config(), - db: b.DB(), + store: b.Store(), enabled: true, } fp.register() @@ -98,44 +61,14 @@ func New(b bot.Bot) *FirstPlugin { return fp } -func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) { - // Get last first entry - var id sql.NullInt64 - var day sql.NullInt64 - var timeEntered sql.NullInt64 - var body sql.NullString - var nick sql.NullString - - err := db.QueryRow(`select - id, max(day), time, body, nick from first - where channel = ? - limit 1; - `, channel).Scan( - &id, - &day, - &timeEntered, - &body, - &nick, - ) - switch { - case err == sql.ErrNoRows || !id.Valid: - log.Info().Msg("No previous first entries") - return nil, nil - case err != nil: - log.Warn().Err(err).Msg("Error on first query row") +func getLastFirst(store *bh.Store, channel string) (*FirstEntry, error) { + fe := &FirstEntry{} + err := store.FindOne(fe, bh.Where("channel").Eq(channel)) + if err != nil { return nil, err } - log.Debug().Msgf("id: %v day %v time %v body %v nick %v", - id, day, timeEntered, body, nick) - return &FirstEntry{ - id: id.Int64, - day: time.Unix(day.Int64, 0), - time: time.Unix(timeEntered.Int64, 0), - channel: channel, - body: body.String, - nick: nick.String, - saved: true, - }, nil + log.Debug().Msgf("id: %v day %v time %v body %v nick %v", fe) + return fe, nil } func Midnight(t time.Time) time.Time { @@ -159,7 +92,7 @@ func (p *FirstPlugin) register() { {Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`(?i)^who'?s on first the most.?$`), Handler: func(r bot.Request) bool { - first, err := getLastFirst(p.db, r.Msg.Channel) + first, err := getLastFirst(p.store, r.Msg.Channel) if first != nil && err == nil { p.leaderboard(r.Conn, r.Msg.Channel) return true @@ -169,7 +102,7 @@ func (p *FirstPlugin) register() { {Kind: bot.Message, IsCmd: false, Regex: regexp.MustCompile(`(?i)^who'?s on first.?$`), Handler: func(r bot.Request) bool { - first, err := getLastFirst(p.db, r.Msg.Channel) + first, err := getLastFirst(p.store, r.Msg.Channel) if first != nil && err == nil { p.announceFirst(r.Conn, first) return true @@ -183,13 +116,13 @@ func (p *FirstPlugin) register() { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "You are not authorized to do that.") return true } - fe, err := getLastFirst(p.db, r.Msg.Channel) + fe, err := getLastFirst(p.store, r.Msg.Channel) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Could not find a first entry.") return true } p.enabled = false - err = fe.delete(p.db) + err = fe.delete(p.store) if err != nil { p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("Could not delete first entry: %s", err)) @@ -215,7 +148,7 @@ func (p *FirstPlugin) register() { return false } - first, err := getLastFirst(p.db, r.Msg.Channel) + first, err := getLastFirst(p.store, r.Msg.Channel) if err != nil { log.Error(). Err(err). @@ -303,7 +236,7 @@ func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) { nick: message.User.Name, } log.Info().Msgf("recordFirst: %+v", first.day) - err := first.save(p.db) + err := first.save(p.store) if err != nil { log.Error().Err(err).Msg("Error saving first entry") return @@ -312,27 +245,31 @@ func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) { } func (p *FirstPlugin) leaderboard(c bot.Connector, ch string) error { - q := `select max(channel) channel, max(nick) nick, count(id) count - from first - group by channel, nick - having channel = ? - order by count desc - limit 3` - res := []struct { - Channel string - Nick string - Count int - }{} - err := p.db.Select(&res, q, ch) + // todo: remove this once we verify stuff + //q := `select max(channel) channel, max(nick) nick, count(id) count + // from first + // group by channel, nick + // having channel = ? + // order by count desc + // limit 3` + groups, err := p.store.FindAggregate(FirstEntry{}, bh.Where("channel").Eq(ch), "channel", "nick") if err != nil { return err } - talismans := []string{":gold-trophy:", ":silver-trophy:", ":bronze-trophy:"} - msg := "First leaderboard:\n" - for i, e := range res { - msg += fmt.Sprintf("%s %d %s\n", talismans[i], e.Count, e.Nick) + if len(groups) != 1 { + return fmt.Errorf("found %d groups but expected 1", len(groups)) } - p.bot.Send(c, bot.Message, ch, msg) + //res := groups[0] + //talismans := []string{":gold-trophy:", ":silver-trophy:", ":bronze-trophy:"} + //msg := "First leaderboard:\n" + //n := res.Count() + //fe := FirstEntry{} + // + //for i, e := range res { + // msg += fmt.Sprintf("%s %d %s\n", talismans[i], e.Count, e.Nick) + //} + //p.bot.Send(c, bot.Message, ch, msg) + // todo: care about this return nil } diff --git a/plugins/goals/goals.go b/plugins/goals/goals.go index c05be62..041750c 100644 --- a/plugins/goals/goals.go +++ b/plugins/goals/goals.go @@ -2,12 +2,12 @@ package goals import ( "fmt" + bh "github.com/timshannon/bolthold" "regexp" "sort" "strconv" "time" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" @@ -19,38 +19,22 @@ import ( type GoalsPlugin struct { b bot.Bot cfg *config.Config - db *sqlx.DB + store *bh.Store handlers bot.HandlerTable } func New(b bot.Bot) *GoalsPlugin { p := &GoalsPlugin{ - b: b, - cfg: b.Config(), - db: b.DB(), + b: b, + cfg: b.Config(), + store: b.Store(), } - p.mkDB() p.registerCmds() b.Register(p, bot.Help, p.help) counter.RegisterUpdate(p.update) return p } -func (p *GoalsPlugin) mkDB() { - _, err := p.db.Exec(`create table if not exists goals ( - id integer primary key, - kind string not null, - who string not null, - what string not null, - amount integer, - - unique (who, what, kind) - )`) - if err != nil { - log.Fatal().Msgf("could not create goals db: %s", err) - } -} - func (p *GoalsPlugin) registerCmds() { p.handlers = bot.HandlerTable{ {Kind: bot.Message, IsCmd: true, @@ -144,7 +128,7 @@ func (p *GoalsPlugin) check(c bot.Connector, ch, kind, what, who string) { } func (p *GoalsPlugin) checkCompetition(c bot.Connector, ch, what, who string) { - items, err := counter.GetItem(p.db, what) + items, err := counter.GetItem(p.store, what) if err != nil || len(items) == 0 { p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) return @@ -204,7 +188,7 @@ func (p *GoalsPlugin) checkGoal(c bot.Connector, ch, what, who string) { Str("id", id). Str("what", what). Msg("looking for item") - item, err := counter.GetUserItem(p.db, nick, id, what) + item, err := counter.GetUserItem(p.store, nick, id, what) if err != nil { p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) return @@ -253,7 +237,7 @@ func parseCmd(r *regexp.Regexp, body string) cmd { } type goal struct { - ID int64 + ID int64 `boltholdid:"ID"` Kind string Who string What string @@ -275,8 +259,7 @@ func (p *GoalsPlugin) newGoal(kind, who, what string, amount int) goal { func (p *GoalsPlugin) getGoal(who, what string) ([]*goal, error) { gs := []*goal{} - err := p.db.Select(&gs, `select * from goals where who = ? and what = ?`, - who, what) + err := p.store.Find(&gs, bh.Where("who").Eq(who).And("what").Eq(what)) if err != nil { return nil, err } @@ -288,8 +271,7 @@ func (p *GoalsPlugin) getGoal(who, what string) ([]*goal, error) { func (p *GoalsPlugin) getGoalKind(kind, who, what string) (*goal, error) { g := &goal{gp: p} - err := p.db.Get(g, `select * from goals where kind = ? and who = ? and what = ?`, - kind, who, what) + err := p.store.FindOne(&g, bh.Where("kind").Eq(kind).And("who").Eq(who).And("what").Eq(what)) if err != nil { return nil, err } @@ -297,15 +279,10 @@ func (p *GoalsPlugin) getGoalKind(kind, who, what string) (*goal, error) { } func (g *goal) Save() error { - res, err := g.gp.db.Exec(`insert or replace into goals (who, what, kind, amount) values (?, ?, ? ,?)`, - g.Who, g.What, g.Kind, g.Amount) + err := g.gp.store.Insert(bh.NextSequence(), &g) if err != nil { return err } - dbID, err := res.LastInsertId() - if err == nil && dbID != g.ID && g.ID == 0 { - g.ID = dbID - } return nil } @@ -313,7 +290,7 @@ func (g goal) Delete() error { if g.ID == -1 { return nil } - _, err := g.gp.db.Exec(`delete from goals where id = ?`, g.ID) + err := g.gp.store.Delete(goal{}, g.ID) return err } diff --git a/plugins/inventory/inventory.go b/plugins/inventory/inventory.go index 6705754..b9f90aa 100644 --- a/plugins/inventory/inventory.go +++ b/plugins/inventory/inventory.go @@ -6,19 +6,20 @@ package inventory import ( "fmt" + bh "github.com/timshannon/bolthold" + "math/rand" "regexp" "strings" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/config" ) type InventoryPlugin struct { - *sqlx.DB + *bh.Store bot bot.Bot config *config.Config handlers bot.HandlerTable @@ -29,7 +30,7 @@ func New(b bot.Bot) *InventoryPlugin { config := b.Config() p := &InventoryPlugin{ - DB: b.DB(), + Store: b.Store(), bot: b, config: config, } @@ -37,15 +38,15 @@ func New(b bot.Bot) *InventoryPlugin { b.RegisterFilter("$item", p.itemFilter) b.RegisterFilter("$giveitem", p.giveItemFilter) - p.DB.MustExec(`create table if not exists inventory ( - item string primary key - );`) - p.register() return p } +type Item struct { + Name string `boltholdid:"Name"` +} + func (p *InventoryPlugin) giveItemFilter(input string) string { for strings.Contains(input, "$giveitem") { item := p.random() @@ -107,72 +108,62 @@ func (p *InventoryPlugin) register() { } func (p *InventoryPlugin) removeRandom() string { - var name string - err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( - &name, - ) + items := []Item{} + err := p.Find(&items, &bh.Query{}) if err != nil { log.Error().Err(err).Msgf("Error finding random entry") return "IAMERROR" } - _, err = p.Exec(`delete from inventory where item=?`, name) - if err != nil { - log.Error().Err(err).Msgf("Error finding random entry") - return "IAMERROR" - } - return name + item := items[rand.Intn(len(items))] + err = p.Delete(item.Name, Item{}) + return item.Name } func (p *InventoryPlugin) count() int { - var output int - err := p.QueryRow(`select count(*) as count from inventory`).Scan(&output) + count, err := p.Store.Count(Item{}, &bh.Query{}) if err != nil { log.Error().Err(err).Msg("Error checking for item") return -1 } - return output + return count } func (p *InventoryPlugin) random() string { - var name string - err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( - &name, - ) + items := []Item{} + err := p.Find(&items, &bh.Query{}) if err != nil { - log.Error().Err(err).Msg("Error finding random entry") + log.Error().Err(err).Msgf("Error finding random entry") return "IAMERROR" } - return name + item := items[rand.Intn(len(items))] + return item.Name } func (p *InventoryPlugin) getAll() []string { - rows, err := p.Queryx(`select item from inventory`) + items := []Item{} + err := p.Find(&items, &bh.Query{}) if err != nil { log.Error().Err(err).Msg("Error getting all items") return []string{} } output := []string{} - for rows.Next() { - var item string - rows.Scan(&item) - output = append(output, item) + for _, item := range items { + output = append(output, item.Name) } - rows.Close() return output } func (p *InventoryPlugin) exists(i string) bool { - var output int - err := p.QueryRow(`select count(*) as count from inventory where item=?`, i).Scan(&output) + count, err := p.Store.Count(Item{}, bh.Where("item").Eq(i)) if err != nil { log.Error().Err(err).Msg("Error checking for item") return false } - return output > 0 + return count > 0 } func (p *InventoryPlugin) remove(i string) { - _, err := p.Exec(`delete from inventory where item=?`, i) + err := p.Store.Delete(i, Item{}) if err != nil { log.Error().Msg("Error inserting new inventory item") } @@ -188,7 +179,7 @@ func (p *InventoryPlugin) addItem(c bot.Connector, m msg.Message, i string) bool if p.count() > max { removed = p.removeRandom() } - _, err := p.Exec(`INSERT INTO inventory (item) values (?)`, i) + err := p.Store.Insert(i, Item{Name: i}) if err != nil { log.Error().Err(err).Msg("Error inserting new inventory item") } diff --git a/plugins/last/last.go b/plugins/last/last.go index 7c0946a..3ebaafa 100644 --- a/plugins/last/last.go +++ b/plugins/last/last.go @@ -2,12 +2,12 @@ package last import ( "fmt" + bh "github.com/timshannon/bolthold" "regexp" "time" "github.com/velour/catbase/config" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" @@ -15,9 +15,9 @@ import ( ) type LastPlugin struct { - b bot.Bot - db *sqlx.DB - c *config.Config + b bot.Bot + store *bh.Store + c *config.Config handlers bot.HandlerTable channels map[string]bool @@ -26,37 +26,14 @@ type LastPlugin struct { func New(b bot.Bot) *LastPlugin { p := &LastPlugin{ b: b, - db: b.DB(), + store: b.Store(), c: b.Config(), channels: map[string]bool{}, } - if err := p.migrate(); err != nil { - panic(err) - } p.register() return p } -func (p *LastPlugin) migrate() error { - tx, err := p.db.Beginx() - if err != nil { - return err - } - _, err = tx.Exec(`create table if not exists last ( - day integer, - channel string not null, - ts int not null, - who string not null, - message string not null, - constraint last_key primary key (day, channel) on conflict replace - )`) - if err != nil { - tx.Rollback() - return err - } - return tx.Commit() -} - func (p *LastPlugin) register() { p.handlers = bot.HandlerTable{ { @@ -133,13 +110,16 @@ func (p *LastPlugin) recordLast(r bot.Request) bool { } } - _, err := p.db.Exec( - `insert into last values (?, ?, ?, ?, ?)`, - day.Unix(), ch, time.Now().Unix(), who, r.Msg.Body) - if err != nil { - log.Error().Err(err).Msgf("Could not record last.") + l := &last{ + Day: day.Unix(), + TS: time.Now().Unix(), + Channel: ch, + Who: who, + Message: r.Msg.Body, } - return false + + // todo: I think last might depend on a key being something more real + return p.store.Insert(bh.NextSequence(), l) == nil } type last struct { @@ -155,9 +135,9 @@ func (p *LastPlugin) yesterdaysLast(ch string) (last, error) { midnight := first.Midnight(time.Now()) q := `select * from last where channel = ? and day < ? and day >= ? order by day limit 1` log.Debug().Str("q", q).Msgf("yesterdaysLast: %d to %d", midnight.Unix(), midnight.Add(-24*time.Hour).Unix()) - err := p.db.Get(&l, q, ch, midnight.Unix(), midnight.Add(-24*time.Hour).Unix()) + err := p.store.FindOne(&l, bh.Where("channel").Eq(ch).And("day").Lt(midnight.Unix()).And("day").Ge(midnight.Add(-24*time.Hour).Unix())) if err != nil { - return l, err + return last{}, err } return l, nil } diff --git a/plugins/newsbid/newsbid.go b/plugins/newsbid/newsbid.go index 354f1a2..c49bb8c 100644 --- a/plugins/newsbid/newsbid.go +++ b/plugins/newsbid/newsbid.go @@ -2,6 +2,7 @@ package newsbid import ( "fmt" + bh "github.com/timshannon/bolthold" "regexp" "sort" "strconv" @@ -14,22 +15,23 @@ import ( ) type NewsBid struct { - bot bot.Bot - db *sqlx.DB - ws *webshit.Webshit + bot bot.Bot + db *sqlx.DB + store *bh.Store + ws *webshit.Webshit } func New(b bot.Bot) *NewsBid { - ws := webshit.NewConfig(b.DB(), webshit.Config{ + ws := webshit.NewConfig(b.Store(), webshit.Config{ HNFeed: b.Config().GetString("webshit.hnfeed", "topstories"), HNLimit: b.Config().GetInt("webshit.hnlimit", 10), BalanceReferesh: b.Config().GetInt("webshit.balancerefresh", 100), HouseName: b.Config().GetString("webshit.housename", "house"), }) p := &NewsBid{ - bot: b, - db: b.DB(), - ws: ws, + bot: b, + store: b.Store(), + ws: ws, } p.bot.RegisterRegexCmd(p, bot.Message, balanceRegex, p.balanceCmd) diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go index 8697285..e6a96c0 100644 --- a/plugins/newsbid/webshit/webshit.go +++ b/plugins/newsbid/webshit/webshit.go @@ -3,6 +3,7 @@ package webshit import ( "bytes" "fmt" + bh "github.com/timshannon/bolthold" "net/url" "strconv" "time" @@ -10,7 +11,6 @@ import ( "github.com/velour/catbase/plugins/newsbid/webshit/hn" "github.com/PuerkitoBio/goquery" - "github.com/jmoiron/sqlx" "github.com/mmcdole/gofeed" "github.com/rs/zerolog/log" ) @@ -23,12 +23,12 @@ type Config struct { } type Webshit struct { - db *sqlx.DB + store *bh.Store config Config } type Bid struct { - ID int + ID int64 `boltholdid:"ID"` User string Title string URL string @@ -37,16 +37,12 @@ type Bid struct { BidStr string PlacedScore int `db:"placed_score"` ProcessedScore int `db:"processed_score"` - Placed int64 - Processed int64 -} - -func (b Bid) PlacedParsed() time.Time { - return time.Unix(b.Placed, 0) + Placed time.Time + Processed bool } type Balance struct { - User string + User string `boltholdid:"User"` Balance int Score int } @@ -65,34 +61,11 @@ type WeeklyResult struct { Score int } -func NewConfig(db *sqlx.DB, cfg Config) *Webshit { - w := &Webshit{db: db, config: cfg} - w.setup() +func NewConfig(store *bh.Store, cfg Config) *Webshit { + w := &Webshit{store: store, config: cfg} return w } -// setup will create any necessary SQL tables and populate them with minimal data -func (w *Webshit) setup() { - w.db.MustExec(`create table if not exists webshit_bids ( - id integer primary key autoincrement, - user string, - title string, - url string, - hnid string, - bid integer, - bidstr string, - placed_score integer, - processed_score integer, - placed integer, - processed integer - )`) - w.db.MustExec(`create table if not exists webshit_balances ( - user string primary key, - balance int, - score int - )`) -} - func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) { stories, published, err := w.GetWeekly() if err != nil { @@ -105,7 +78,7 @@ func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) { } var bids []Bid - if err = w.db.Select(&bids, `select user,title,url,hnid,bid,bidstr from webshit_bids where processed=0`); err != nil { + if err = w.store.Find(&bids, bh.Where("processed").Eq(false)); err != nil { return nil, 0, err } @@ -133,14 +106,20 @@ func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) { } // Delete all those bids - if _, err = w.db.Exec(`update webshit_bids set processed=? where processed=0`, - time.Now().Unix()); err != nil { + if err = w.store.UpdateMatching(Bid{}, bh.Where("processed").Eq(false), func(record interface{}) error { + r := record.(*Bid) + r.Processed = true + return w.store.Update(r.ID, r) + }); err != nil { return nil, 0, err } // Set all balances to 100 - if _, err = w.db.Exec(`update webshit_balances set balance=?`, - w.config.BalanceReferesh); err != nil { + if err = w.store.UpdateMatching(Balance{}, &bh.Query{}, func(record interface{}) error { + r := record.(*Balance) + r.Balance = w.config.BalanceReferesh + return w.store.Update(r.User, r) + }); err != nil { return nil, 0, err } @@ -156,7 +135,7 @@ func (w *Webshit) checkBids(bids []Bid, storyMap map[string]hn.Item) []WeeklyRes houseScore := 0 for _, b := range bids { - score := w.GetScore(b.User) + score := w.GetBalance(b.User).Score if _, ok := wr[b.User]; !ok { wr[b.User] = WeeklyResult{ User: b.User, @@ -193,7 +172,7 @@ func (w *Webshit) checkBids(bids []Bid, storyMap map[string]hn.Item) []WeeklyRes wr[houseName] = WeeklyResult{ User: houseName, - Score: w.GetScore(houseName) + houseScore, + Score: w.GetBalance(houseName).Score + houseScore, Won: houseScore, } @@ -240,29 +219,22 @@ func (w *Webshit) GetWeekly() (hn.Items, *time.Time, error) { // GetBalances returns the current balance for all known users // Any unknown user has a default balance on their first bid -func (w *Webshit) GetBalance(user string) int { - q := `select balance from webshit_balances where user=?` - var balance int - err := w.db.Get(&balance, q, user) +func (w *Webshit) GetBalance(user string) Balance { + var balance Balance + err := w.store.Get(user, &balance) if err != nil { - return 100 + return Balance{ + User: user, + Balance: 100, + Score: 0, + } } return balance } -func (w *Webshit) GetScore(user string) int { - q := `select score from webshit_balances where user=?` - var score int - err := w.db.Get(&score, q, user) - if err != nil { - return 0 - } - return score -} - func (w *Webshit) GetAllBids() ([]Bid, error) { var bids []Bid - err := w.db.Select(&bids, `select * from webshit_bids where processed=0`) + err := w.store.Find(&bids, bh.Where("processed").Eq(false)) if err != nil { return nil, err } @@ -271,7 +243,7 @@ func (w *Webshit) GetAllBids() ([]Bid, error) { func (w *Webshit) GetAllBalances() (Balances, error) { var balances Balances - err := w.db.Select(&balances, `select * from webshit_balances`) + err := w.store.Find(&balances, &bh.Query{}) if err != nil { return nil, err } @@ -280,7 +252,7 @@ func (w *Webshit) GetAllBalances() (Balances, error) { // Bid allows a user to place a bid on a particular story func (w *Webshit) Bid(user string, amount int, bidStr, URL string) (Bid, error) { - bal := w.GetBalance(user) + bal := w.GetBalance(user).Balance if amount < 0 { return Bid{}, fmt.Errorf("cannot bid less than 0") } @@ -292,32 +264,24 @@ func (w *Webshit) Bid(user string, amount int, bidStr, URL string) (Bid, error) return Bid{}, err } - ts := time.Now().Unix() - - tx := w.db.MustBegin() - _, err = tx.Exec(`insert into webshit_bids (user,title,url,hnid,bid,bidstr,placed,processed,placed_score,processed_score) values (?,?,?,?,?,?,?,0,?,0)`, - user, story.Title, story.URL, story.ID, amount, bidStr, ts, story.Score) - if err != nil { - if err := tx.Rollback(); err != nil { - return Bid{}, err - } - return Bid{}, err - } - q := `insert into webshit_balances (user,balance,score) values (?,?,0) - on conflict(user) do update set balance=?` - _, err = tx.Exec(q, user, bal-amount, bal-amount) - if err != nil { - tx.Rollback() - return Bid{}, err - } - err = tx.Commit() - - return Bid{ + bid := Bid{ User: user, Title: story.Title, URL: story.URL, - Placed: ts, - }, err + Placed: time.Now(), + } + + err = w.store.Insert(Bid{}, bid) + if err != nil { + return Bid{}, err + } + + err = w.store.Upsert(user, bal) + if err != nil { + return Bid{}, err + } + + return bid, nil } // getStoryByURL scrapes the URL for a title @@ -337,18 +301,14 @@ func (w *Webshit) getStoryByURL(URL string) (hn.Item, error) { } func (w *Webshit) updateScores(results []WeeklyResult) error { - tx := w.db.MustBegin() for _, res := range results { - if _, err := tx.Exec(`insert into webshit_balances (user, balance, score) values (?, 0, ?) on conflict(user) do update set score=excluded.score`, - res.User, res.Score); err != nil { - if err := tx.Rollback(); err != nil { - return err - } + bal := w.GetBalance(res.User) + bal.Score = res.Score + if err := w.store.Insert(res.User, bal); err != nil { return err } } - err := tx.Commit() - return err + return nil } func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult { diff --git a/plugins/quotegame/quotegame.go b/plugins/quotegame/quotegame.go index 79ee980..e3ccc73 100644 --- a/plugins/quotegame/quotegame.go +++ b/plugins/quotegame/quotegame.go @@ -28,7 +28,6 @@ func New(b bot.Bot) *QuoteGame { p := &QuoteGame{ b: b, c: b.Config(), - db: b.DB(), currentGame: nil, } p.register() diff --git a/plugins/remember/remember.go b/plugins/remember/remember.go index fd3fc35..f9b497e 100644 --- a/plugins/remember/remember.go +++ b/plugins/remember/remember.go @@ -2,29 +2,30 @@ package remember import ( "fmt" + bh "github.com/timshannon/bolthold" + "math/rand" "regexp" "strings" "time" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/plugins/fact" ) type RememberPlugin struct { - bot bot.Bot - log map[string][]msg.Message - db *sqlx.DB + bot bot.Bot + log map[string][]msg.Message + store *bh.Store } func New(b bot.Bot) *RememberPlugin { p := &RememberPlugin{ - bot: b, - log: make(map[string][]msg.Message), - db: b.DB(), + bot: b, + log: make(map[string][]msg.Message), + store: b.Store(), } b.RegisterRegex(p, bot.Message, rememberRegex, p.rememberCmd) @@ -78,7 +79,7 @@ func (p *RememberPlugin) rememberCmd(r bot.Request) bool { Accessed: time.Now(), Count: 0, } - if err := fact.Save(p.db); err != nil { + if err := fact.Save(p.store); err != nil { log.Error().Err(err) p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Tell somebody I'm broke.") } @@ -109,32 +110,32 @@ func (p *RememberPlugin) help(c bot.Connector, kind bot.Kind, message msg.Messag return true } -// deliver a random quote out of the db. +func (p *RememberPlugin) randQuote() string { + return RandQuote(p.store) +} + +// AllQuotesFrom delivers quotes out of the db from who. +func AllQuotesFrom(store *bh.Store, who string) []fact.Factoid { + allQuotes := []fact.Factoid{} + err := store.Find(&allQuotes, bh.Where("fact").Eq(who+" quotes")) + if err != nil { + log.Error().Err(err).Msg("Error getting quotes") + return []fact.Factoid{} + } + return allQuotes +} + +// RandQuote delivers a random quote out of the db. // Note: this is the same cache for all channels joined. This plugin needs to be // expanded to have this function execute a quote for a particular channel -func (p *RememberPlugin) randQuote() string { - - var f fact.Factoid - var tmpCreated int64 - var tmpAccessed int64 - err := p.db.QueryRow(`select * from factoid where fact like '%quotes' - order by random() limit 1;`).Scan( - &f.ID, - &f.Fact, - &f.Tidbit, - &f.Verb, - &f.Owner, - &tmpCreated, - &tmpAccessed, - &f.Count, - ) +func RandQuote(store *bh.Store) string { + allQuotes := []fact.Factoid{} + err := store.Find(&allQuotes, bh.Where("fact").RegExp(regexp.MustCompile(`.+ quotes$`))) if err != nil { log.Error().Err(err).Msg("Error getting quotes") return "I had a problem getting your quote." } - f.Created = time.Unix(tmpCreated, 0) - f.Accessed = time.Unix(tmpAccessed, 0) - + f := allQuotes[rand.Intn(len(allQuotes))] return f.Tidbit } diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go index a3b23b8..a579301 100644 --- a/plugins/reminder/reminder.go +++ b/plugins/reminder/reminder.go @@ -3,23 +3,23 @@ package reminder import ( - "errors" - "fmt" - "strconv" - "strings" - "sync" - "time" - - "github.com/jmoiron/sqlx" - "github.com/rs/zerolog/log" - "github.com/olebedev/when" "github.com/olebedev/when/rules/common" "github.com/olebedev/when/rules/en" - + bh "github.com/timshannon/bolthold" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" "github.com/velour/catbase/config" + "sync" + "time" +) + +import ( + "fmt" + "github.com/rs/zerolog/log" + "strconv" + "strings" + "github.com/velour/catbase/plugins/sms" ) @@ -29,7 +29,7 @@ const ( type ReminderPlugin struct { bot bot.Bot - db *sqlx.DB + store *bh.Store mutex *sync.Mutex timer *time.Timer config *config.Config @@ -46,17 +46,6 @@ type Reminder struct { } func New(b bot.Bot) *ReminderPlugin { - if _, err := b.DB().Exec(`create table if not exists reminders ( - id integer primary key, - fromWho string, - toWho string, - what string, - remindWhen string, - channel string - );`); err != nil { - log.Fatal().Err(err) - } - dur, _ := time.ParseDuration("1h") timer := time.NewTimer(dur) timer.Stop() @@ -67,7 +56,7 @@ func New(b bot.Bot) *ReminderPlugin { plugin := &ReminderPlugin{ bot: b, - db: b.DB(), + store: b.Store(), mutex: &sync.Mutex{}, timer: timer, config: b.Config(), @@ -225,45 +214,22 @@ func (p *ReminderPlugin) help(c bot.Connector, kind bot.Kind, message msg.Messag func (p *ReminderPlugin) getNextReminder() *Reminder { p.mutex.Lock() defer p.mutex.Unlock() - rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;") + + reminder := Reminder{} + res, err := p.store.FindAggregate(Reminder{}, &bh.Query{}, "") if err != nil { log.Error().Err(err) return nil } - defer rows.Close() + res[0].Max("remindWhen", &reminder) - once := false - var reminder *Reminder - for rows.Next() { - if once { - log.Debug().Msg("somehow got multiple rows") - } - reminder = &Reminder{} - - var when string - err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when, &reminder.channel) - if err != nil { - log.Error().Err(err) - return nil - } - reminder.when, err = time.Parse(TIMESTAMP, when) - if err != nil { - log.Error().Err(err) - return nil - } - - once = true - } - - return reminder + return &reminder } func (p *ReminderPlugin) addReminder(reminder *Reminder) error { p.mutex.Lock() defer p.mutex.Unlock() - _, err := p.db.Exec(`insert into reminders (fromWho, toWho, what, remindWhen, channel) values (?, ?, ?, ?, ?);`, - reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel) - + err := p.store.Insert(bh.NextSequence(), reminder) if err != nil { log.Error().Err(err) } @@ -273,29 +239,21 @@ func (p *ReminderPlugin) addReminder(reminder *Reminder) error { func (p *ReminderPlugin) deleteReminder(id int64) error { p.mutex.Lock() defer p.mutex.Unlock() - res, err := p.db.Exec(`delete from reminders where id = ?;`, id) - if err != nil { - log.Error().Err(err) - } else { - if affected, err := res.RowsAffected(); err != nil { - return err - } else if affected != 1 { - return errors.New("didn't delete any rows") - } - } + err := p.store.Delete(id, Reminder{}) return err } -func (p *ReminderPlugin) getRemindersFormatted(filter string) (string, error) { +func (p *ReminderPlugin) getRemindersFormatted(filter, who string) (string, error) { max := p.config.GetInt("Reminder.MaxList", 25) - queryString := fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders %s order by remindWhen asc limit %d;", filter, max) - countString := fmt.Sprintf("select COUNT(*) from reminders %s;", filter) p.mutex.Lock() defer p.mutex.Unlock() - var total int - err := p.db.Get(&total, countString) + q := bh.Where(filter).Eq(who) + if filter == "" || who == "" { + q = &bh.Query{} + } + total, err := p.store.Count(Reminder{}, q) if err != nil { log.Error().Err(err) return "", nil @@ -305,43 +263,36 @@ func (p *ReminderPlugin) getRemindersFormatted(filter string) (string, error) { return "no pending reminders", nil } - rows, err := p.db.Query(queryString) + reminders := []Reminder{} + err = p.store.Find(&reminders, q.SortBy("id").Limit(max)) if err != nil { log.Error().Err(err) return "", nil } - defer rows.Close() - reminders := "" - counter := 1 - reminder := &Reminder{} - for rows.Next() { - var when string - err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when) - if err != nil { - return "", err - } - reminders += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, when, reminder.id) + txt := "" + for counter, reminder := range reminders { + txt += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, reminder.when, reminder.id) counter++ } remaining := total - max if remaining > 0 { - reminders += fmt.Sprintf("...%d more...\n", remaining) + txt += fmt.Sprintf("...%d more...\n", remaining) } - return reminders, nil + return txt, nil } func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) { - return p.getRemindersFormatted("") + return p.getRemindersFormatted("", "") } func (p *ReminderPlugin) getAllRemindersFromMeFormatted(channel, me string) (string, error) { - return p.getRemindersFormatted(fmt.Sprintf("where fromWho = '%s'", me)) + return p.getRemindersFormatted("fromWho", me) } func (p *ReminderPlugin) getAllRemindersToMeFormatted(channel, me string) (string, error) { - return p.getRemindersFormatted(fmt.Sprintf("where toWho = '%s'", me)) + return p.getRemindersFormatted("toWho", me) } func (p *ReminderPlugin) queueUpNextReminder() { diff --git a/plugins/rest/rest.go b/plugins/rest/rest.go index 2dccf6c..628b4f8 100644 --- a/plugins/rest/rest.go +++ b/plugins/rest/rest.go @@ -3,10 +3,10 @@ package rest import ( "bytes" "crypto/sha512" - "database/sql" "encoding/json" "errors" "fmt" + bh "github.com/timshannon/bolthold" "io/ioutil" "net/http" "net/url" @@ -18,13 +18,12 @@ import ( "github.com/itchyny/gojq" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" ) type RestPlugin struct { - b bot.Bot - db *sqlx.DB + b bot.Bot + store *bh.Store handlers bot.HandlerTable } @@ -50,29 +49,13 @@ var postProcessors = map[string]postProcessor{ func New(b bot.Bot) *RestPlugin { p := &RestPlugin{ b: b, - db: b.DB(), + store: b.Store(), handlers: bot.HandlerTable{}, } - p.setupDB() p.register() return p } -func (p *RestPlugin) setupDB() { - tx := p.db.MustBegin() - tx.MustExec(` - create table if not exists wires ( - id integer primary key autoincrement, - url text not null, - parse_regex text not null, - return_field text not null, - body text not null - )`) - if err := tx.Commit(); err != nil { - panic(err) - } -} - func (p *RestPlugin) register() { p.handlers = bot.HandlerTable{ bot.HandlerSpec{Kind: bot.Message, IsCmd: true, @@ -161,7 +144,7 @@ func (s *ScanableURL) Scan(src interface{}) error { type wire struct { // ID - ID sql.NullInt64 + ID int64 `boltholdIndex:"ID"` // The URL to make a request to URL ScanableURL // The regex which will trigger this REST action @@ -182,40 +165,28 @@ func (w wire) String() string { func (p *RestPlugin) getWires() ([]wire, error) { wires := []wire{} - err := p.db.Select(&wires, `select * from wires`) + err := p.store.Find(&wires, &bh.Query{}) return wires, err } func (p *RestPlugin) deleteWire(id int64) error { - _, err := p.db.Exec(`delete from wires where id=?`, id) + err := p.store.Delete(id, wire{}) return err } -func (w *wire) Update(db *sqlx.DB) error { - if !w.ID.Valid { - return w.Save(db) +func (w *wire) Update(store *bh.Store) error { + if w.ID == -1 { + return w.Save(store) } - id, _ := w.ID.Value() - _, err := db.Exec(`update wires set url=?, parse_regex=?, return_field=? where id=?`, - w.URL.String(), w.ParseRegex.String(), w.ReturnField, id) + err := store.Update(w.ID, w) return err } -func (w *wire) Save(db *sqlx.DB) error { - if w.ID.Valid { - return w.Update(db) +func (w *wire) Save(store *bh.Store) error { + if w.ID > -1 { + return w.Update(store) } - res, err := db.Exec(`insert into wires (url, parse_regex, return_field) values (?, ?, ?)`, - w.URL.String(), w.ParseRegex.String(), w.ReturnField) - if err != nil { - return err - } - id, err := res.LastInsertId() - if err != nil { - return err - } - _ = w.ID.Scan(id) - return nil + return store.Insert(bh.NextSequence(), &w) } func (p *RestPlugin) listWires(r bot.Request) bool { @@ -227,8 +198,7 @@ func (p *RestPlugin) listWires(r bot.Request) bool { } msg = "Current wires:" for _, w := range wires { - id, _ := w.ID.Value() - msg += fmt.Sprintf("\n\t%d: `%s` => %s", id, w.ParseRegex, w.URL) + msg += fmt.Sprintf("\n\t%d: `%s` => %s", w.ID, w.ParseRegex, w.URL) } SEND: p.b.Send(r.Conn, bot.Message, r.Msg.Channel, msg) @@ -277,7 +247,7 @@ func (p *RestPlugin) handleWire(r bot.Request) bool { msg = err.Error() goto SEND } - err = w.Save(p.db) + err = w.Save(p.store) if err != nil { msg = err.Error() goto SEND diff --git a/plugins/roles/role.go b/plugins/roles/role.go index 1ab7ccf..fb10d6e 100644 --- a/plugins/roles/role.go +++ b/plugins/roles/role.go @@ -1,59 +1,55 @@ package roles import ( - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" + bh "github.com/timshannon/bolthold" ) type Role struct { - ID int64 `json:"id"` + ID int64 `json:"id" boltholdIndex:"ID"` Name string `json:"name"` Offering string `json:"offering"` - *sqlx.DB + *bh.Store } -func NewRole(db *sqlx.DB, name, offering string) *Role { +func NewRole(store *bh.Store, name, offering string) *Role { return &Role{ Name: name, Offering: offering, - DB: db, + Store: store, } } func (r *Role) Save() error { - q := `insert or replace into roles (name, offering) values (?, ?)` - res, err := r.Exec(q, r.Name, r.Offering) - if checkErr(err) { - return err + if r.ID == -1 { + r.Store.Upsert(bh.NextSequence(), r) + } else { + r.Store.Upsert(r.ID, r) } - id, err := res.LastInsertId() - if checkErr(err) { - return err - } - r.ID = id return nil } -func getRolesByOffering(db *sqlx.DB, offering string) ([]*Role, error) { +func getRolesByOffering(store *bh.Store, offering string) ([]*Role, error) { roles := []*Role{} - err := db.Select(&roles, `select * from roles where offering=?`, offering) + err := store.Find(&roles, bh.Where("offering").Eq(offering)) if checkErr(err) { return nil, err } for _, r := range roles { - r.DB = db + r.Store = store + r.Store = store } return roles, nil } -func getRole(db *sqlx.DB, name, offering string) (*Role, error) { +func getRole(store *bh.Store, name, offering string) (*Role, error) { role := &Role{} - err := db.Get(role, `select * from roles where role=? and offering=?`, role, offering) + err := store.Find(&role, bh.Where("role").Eq(role).And("offering").Eq(offering)) if checkErr(err) { return nil, err } - role.DB = db + role.Store = store return role, nil } diff --git a/plugins/roles/roles.go b/plugins/roles/roles.go index 9dbcb7f..fc24c3b 100644 --- a/plugins/roles/roles.go +++ b/plugins/roles/roles.go @@ -2,27 +2,27 @@ package roles import ( "fmt" + bh "github.com/timshannon/bolthold" "regexp" "strings" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" "github.com/velour/catbase/config" ) type RolesPlugin struct { - b bot.Bot - c *config.Config - db *sqlx.DB - h bot.HandlerTable + b bot.Bot + c *config.Config + store *bh.Store + h bot.HandlerTable } func New(b bot.Bot) *RolesPlugin { p := &RolesPlugin{ - b: b, - c: b.Config(), - db: b.DB(), + b: b, + c: b.Config(), + store: b.Store(), } p.RegisterWeb() p.Register() diff --git a/plugins/secrets/secrets.go b/plugins/secrets/secrets.go index 9e10954..9af80ac 100644 --- a/plugins/secrets/secrets.go +++ b/plugins/secrets/secrets.go @@ -4,10 +4,10 @@ import ( "embed" "encoding/json" "fmt" + bh "github.com/timshannon/bolthold" "net/http" "github.com/go-chi/chi/v5" - "github.com/jmoiron/sqlx" "github.com/rs/zerolog/log" "github.com/velour/catbase/bot" "github.com/velour/catbase/config" @@ -17,16 +17,16 @@ import ( var embeddedFS embed.FS type SecretsPlugin struct { - b bot.Bot - c *config.Config - db *sqlx.DB + b bot.Bot + c *config.Config + store *bh.Store } func New(b bot.Bot) *SecretsPlugin { p := &SecretsPlugin{ - b: b, - c: b.Config(), - db: b.DB(), + b: b, + c: b.Config(), + store: b.Store(), } p.registerWeb() return p @@ -47,8 +47,7 @@ func (p *SecretsPlugin) registerWeb() { } func (p *SecretsPlugin) registerSecret(key, value string) error { - q := `insert into secrets (key, value) values (?, ?)` - _, err := p.db.Exec(q, key, value) + err := p.store.Insert(key, config.Secret{key, value}) if err != nil { return err } @@ -56,8 +55,7 @@ func (p *SecretsPlugin) registerSecret(key, value string) error { } func (p *SecretsPlugin) removeSecret(key string) error { - q := `delete from secrets where key=?` - _, err := p.db.Exec(q, key) + err := p.store.Delete(key, config.Secret{}) if err != nil { return err } @@ -65,8 +63,7 @@ func (p *SecretsPlugin) removeSecret(key string) error { } func (p *SecretsPlugin) updateSecret(key, value string) error { - q := `update secrets set value=? where key=?` - _, err := p.db.Exec(q, value, key) + err := p.store.Update(key, config.Secret{key, value}) if err != nil { return err } diff --git a/plugins/sms/sms.go b/plugins/sms/sms.go index 9f81e7b..4f8f543 100644 --- a/plugins/sms/sms.go +++ b/plugins/sms/sms.go @@ -2,6 +2,7 @@ package sms import ( "fmt" + bh "github.com/timshannon/bolthold" "net/http" "regexp" "strings" @@ -18,8 +19,14 @@ import ( var plugin *SMSPlugin type SMSPlugin struct { - b bot.Bot - c *config.Config + b bot.Bot + c *config.Config + store *bh.Store +} + +type Person struct { + Who string + Num string } func New(b bot.Bot) *SMSPlugin { @@ -27,10 +34,10 @@ func New(b bot.Bot) *SMSPlugin { return plugin } plugin = &SMSPlugin{ - b: b, - c: b.Config(), + b: b, + c: b.Config(), + store: b.Store(), } - plugin.setup() plugin.registerWeb() b.RegisterRegex(plugin, bot.Message, deleteRegex, plugin.deleteCmd) @@ -155,51 +162,24 @@ func (p *SMSPlugin) receive(w http.ResponseWriter, r *http.Request) { } func (p *SMSPlugin) add(who, num string) error { - tx, err := p.b.DB().Beginx() - if err != nil { - return err - } - _, err = tx.Exec(`insert or replace into sms (who, num) values (?, ?)`, who, num) - if err != nil { - return err - } - err = tx.Commit() - return err + person := Person{who, num} + return p.store.Upsert(who, person) } func (p *SMSPlugin) delete(who string) error { - tx, err := p.b.DB().Beginx() - if err != nil { - return err - } - _, err = tx.Exec(`delete from sms where who=?`, who) - if err != nil { - return err - } - err = tx.Commit() - return err + return p.store.Delete(who, Person{}) } func (p *SMSPlugin) get(who string) (string, error) { - num := "" - err := p.b.DB().Get(&num, `select num from sms where who=?`, who) - return num, err + person := Person{} + err := p.store.Get(who, &person) + return person.Num, err } func (p *SMSPlugin) getByNumber(num string) (string, error) { - who := "" - err := p.b.DB().Get(&who, `select who from sms where num=?`, num) - return who, err -} - -func (p *SMSPlugin) setup() { - _, err := p.b.DB().Exec(`create table if not exists sms ( - who string primary key, - num string - )`) - if err != nil { - log.Fatal().Err(err).Msgf("could not create sms table") - } + person := Person{} + err := p.store.Find(&person, bh.Where("num").Eq(num)) + return person.Who, err } func (p *SMSPlugin) deleteCmd(r bot.Request) bool { diff --git a/plugins/tell/tell.go b/plugins/tell/tell.go index 22e937d..d0e0738 100644 --- a/plugins/tell/tell.go +++ b/plugins/tell/tell.go @@ -2,12 +2,11 @@ package tell import ( "fmt" + bh "github.com/timshannon/bolthold" "strings" "github.com/rs/zerolog/log" - "github.com/jmoiron/sqlx" - "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) @@ -15,49 +14,37 @@ import ( type delayedMsg string type TellPlugin struct { - b bot.Bot - db *sqlx.DB + b bot.Bot + store *bh.Store } func New(b bot.Bot) *TellPlugin { - tp := &TellPlugin{b, b.DB()} + tp := &TellPlugin{b, b.Store()} b.Register(tp, bot.Message, tp.message) - tp.createDB() return tp } type tell struct { - ID int + ID int `boltholdIndex:"ID"` Who string What string } -func (t *TellPlugin) createDB() { - q := `create table if not exists tell ( - id integer primary key autoincrement, - who string, - what string - )` - t.db.MustExec(q) -} - func (t *TellPlugin) getTells() []tell { result := []tell{} - q := `select * from tell` - t.db.Select(&result, q) + t.store.Find(&result, &bh.Query{}) return result } func (t *TellPlugin) rmTell(entry tell) { - q := `delete from tell where id=?` - if _, err := t.db.Exec(q, entry.ID); err != nil { + if err := t.store.Delete(entry.ID, tell{}); err != nil { log.Error().Err(err).Msg("could not remove tell") } } func (t *TellPlugin) addTell(who, what string) error { - q := `insert into tell (who, what) values (?, ?)` - _, err := t.db.Exec(q, who, what) + tell := tell{Who: who, What: what} + err := t.store.Insert(bh.NextSequence(), tell) if err != nil { log.Error().Err(err).Msg("could not add tell") }