This commit is contained in:
Chris Sexton 2021-12-20 12:40:10 -05:00
parent d89569cdc6
commit fafbd2ce64
33 changed files with 1010 additions and 1492 deletions

View File

@ -8,9 +8,9 @@ jobs:
steps: steps:
- name: Set up Go 1.16 - name: Set up Go 1.16
uses: actions/setup-go@v1 uses: actions/setup-go@v2
with: with:
go-version: 1.16.x go-version: '^1.18'
id: go id: go
- name: Check out code into the Go module directory - name: Check out code into the Go module directory

View File

@ -4,6 +4,7 @@ package bot
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"math/rand" "math/rand"
"net/http" "net/http"
"reflect" "reflect"
@ -13,7 +14,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/history" "github.com/velour/catbase/bot/history"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -111,8 +111,6 @@ func New(config *config.Config, connector Connector) Bot {
history: history.New(historySz), history: history.New(historySz),
} }
bot.migrateDB()
bot.RefreshPluginBlacklist() bot.RefreshPluginBlacklist()
bot.RefreshPluginWhitelist() bot.RefreshPluginWhitelist()
@ -161,33 +159,13 @@ func (b *bot) Config() *config.Config {
return b.config return b.config
} }
func (b *bot) DB() *sqlx.DB { // TODO: remove
return b.config.DB //func (b *bot) DB() *sqlx.DB {
} // return b.config.DB()
//}
// Create any tables if necessary based on version of DB func (b *bot) Store() *bh.Store {
// Plugins should create their own tables, these are only for official bot stuff return b.config.Store()
// 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")
}
} }
// Adds a constructed handler to the bots handlers list // Adds a constructed handler to the bots handlers list
@ -330,13 +308,19 @@ func (b *bot) SetQuiet(status bool) {
b.quiet = status b.quiet = status
} }
// RefreshPluginBlacklist loads data for which plugins are disabled for particular channels type blacklistItem struct {
func (b *bot) RefreshPluginBlacklist() error {
blacklistItems := []struct {
Channel string Channel string
Name string Name string
}{} }
if err := b.DB().Select(&blacklistItems, `select channel, name from pluginBlacklist`); err != nil {
type whitelistItem struct {
Name string
}
// RefreshPluginBlacklist loads data for which plugins are disabled for particular channels
func (b *bot) RefreshPluginBlacklist() error {
blacklistItems := []blacklistItem{}
if err := b.Store().Find(&blacklistItems, &bh.Query{}); err != nil {
return fmt.Errorf("%w", err) return fmt.Errorf("%w", err)
} }
b.pluginBlacklist = make(map[string]bool) b.pluginBlacklist = make(map[string]bool)
@ -349,12 +333,10 @@ func (b *bot) RefreshPluginBlacklist() error {
// RefreshPluginWhitelist loads data for which plugins are enabled // RefreshPluginWhitelist loads data for which plugins are enabled
func (b *bot) RefreshPluginWhitelist() error { func (b *bot) RefreshPluginWhitelist() error {
whitelistItems := []struct { whitelistItems := []whitelistItem{
Name string
}{
{Name: "admin"}, // we must always ensure admin is on! {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) return fmt.Errorf("%w", err)
} }
b.pluginWhitelist = make(map[string]bool) b.pluginWhitelist = make(map[string]bool)
@ -411,9 +393,8 @@ func (b *bot) CheckPassword(secret, password string) bool {
secret = parts[0] secret = parts[0]
password = parts[1] password = parts[1]
} }
q := `select encoded_pass from apppass where secret = ?`
encodedPasswords := [][]byte{} encodedPasswords := [][]byte{}
b.DB().Select(&encodedPasswords, q, secret) b.Store().Find(&encodedPasswords, bh.Where("secret").Eq(secret))
for _, p := range encodedPasswords { for _, p := range encodedPasswords {
if err := bcrypt.CompareHashAndPassword(p, []byte(password)); err == nil { if err := bcrypt.CompareHashAndPassword(p, []byte(password)); err == nil {
return true return true

View File

@ -4,10 +4,10 @@ package bot
import ( import (
"bytes" "bytes"
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
"net/http" "net/http"
@ -216,27 +216,33 @@ func (b *bot) Filter(message msg.Message, input string) string {
return input return input
} }
type variable struct {
Text string
}
func (b *bot) getVar(varName string) (string, error) { func (b *bot) getVar(varName string) (string, error) {
var text string var v []variable
err := b.DB().Get(&text, `select value from variables where name=? order by random() limit 1`, varName) err := b.Store().Find(&v, bh.Where("name").Eq(varName))
switch { if err != nil {
case err == sql.ErrNoRows:
return "", fmt.Errorf("No factoid found")
case err != nil:
log.Fatal().Err(err).Msg("getVar error") 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) { func (b *bot) listVars(conn Connector, channel string, parts []string) {
var variables []string var variables []variable
err := b.DB().Select(&variables, `select name from variables group by name`) err := b.Store().Find(&variables, &bh.Query{})
if err != nil { if err != nil {
log.Fatal().Err(err) log.Fatal().Err(err)
} }
msg := "I know: $who, $someone, $digit, $nonzero, $time, $now, $msg" msg := "I know: $who, $someone, $digit, $nonzero, $time, $now, $msg"
if len(variables) > 0 { out := []string{}
msg += ", " + strings.Join(variables, ", ") for _, v := range variables {
out = append(out, v.Text)
}
if len(out) > 0 {
msg += ", " + strings.Join(out, ", ")
} }
b.Send(conn, Message, channel, msg) b.Send(conn, Message, channel, msg)
} }

View File

@ -3,11 +3,10 @@
package bot package bot
import ( import (
bh "github.com/timshannon/bolthold"
"net/http" "net/http"
"regexp" "regexp"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -81,7 +80,8 @@ type Bot interface {
Config() *config.Config Config() *config.Config
// DB gives access to the current database // DB gives access to the current database
DB() *sqlx.DB //DB() *sqlx.DB
Store() *bh.Store
// Who lists users in a particular channel // Who lists users in a particular channel
// The channel should be represented as an ID for slack (check the connector for details) // The channel should be represented as an ID for slack (check the connector for details)

View File

@ -29,7 +29,7 @@ type MockBot struct {
} }
func (mb *MockBot) Config() *config.Config { return mb.Cfg } 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) Who(string) []user.User { return []user.User{} }
func (mb *MockBot) WhoAmI() string { return "tester" } func (mb *MockBot) WhoAmI() string { return "tester" }
func (mb *MockBot) DefaultConnector() Connector { return nil } func (mb *MockBot) DefaultConnector() Connector { return nil }

View File

@ -3,15 +3,15 @@
package config package config
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"regexp"
"strconv" "strconv"
"strings" "strings"
sqlite3 "github.com/mattn/go-sqlite3" _ "modernc.org/sqlite"
bh "github.com/timshannon/bolthold"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -20,20 +20,34 @@ import (
// Config stores any system-wide startup information that cannot be easily configured via // Config stores any system-wide startup information that cannot be easily configured via
// the database // the database
type Config struct { type Config struct {
*sqlx.DB db *sqlx.DB
store *bh.Store
DBFile string DBFile string
secrets map[string]Secret secrets map[string]Secret
} }
// Secret is a config value that is loaded permanently and not ever displayed // Value is a config value that is loaded permanently and not ever displayed
type Secret struct { type Value struct {
// Key is the key field of the table // Key is the key field of the table
Key string `db:"key"` Key string `db:"key"`
// Value represents the secret that must not be shared // Value represents the secret that must not be shared
Value string `db:"value"` 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 // GetFloat64 returns the config value for a string key
// It will first look in the env vars for the key // It will first look in the env vars for the key
// It will check the db for the key if an env DNE // 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 { if v, found := c.secrets[key]; found {
return v.Value return v.Value
} }
var configValue string var configValue Value
q := `select value from config where key=?` err := c.store.Get(key, &configValue)
err := c.DB.Get(&configValue, q, key)
if err != nil { if err != nil {
log.Debug().Msgf("WARN: Key %s is empty", key) log.Debug().Msgf("WARN: Key %s is empty", key)
return fallback return fallback
} }
return configValue return configValue.Value
} }
func (c *Config) GetMap(key string, fallback map[string]string) map[string]string { func (c *Config) GetMap(key string, fallback map[string]string) map[string]string {
@ -136,48 +149,23 @@ func (c *Config) GetArray(key string, fallback []string) []string {
} }
func (c *Config) Unset(key string) error { func (c *Config) Unset(key string) error {
q := `delete from config where key=?` err := c.store.Delete(key, &Value{})
tx, err := c.Begin()
if err != nil {
return err return err
} }
_, err = tx.Exec(q, key)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
// Set changes the value for a configuration in the database // Set changes the value for a configuration in the database
// Note, this is always a string. Use the SetArray for an array helper // Note, this is always a string. Use the SetArray for an array helper
func (c *Config) Set(key, value string) error { func (c *Config) Set(key, value string) error {
key = strings.ToLower(key) key = strings.ToLower(key)
value = strings.Trim(value, "`") value = strings.Trim(value, "`")
q := `insert into config (key,value) values (?, ?)
on conflict(key) do update set value=?;` err := c.store.Update(key, Value{key, value})
tx, err := c.Begin()
if err != nil {
return err return err
} }
_, err = tx.Exec(q, key, value, value)
if err != nil {
return err
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (c *Config) RefreshSecrets() error { func (c *Config) RefreshSecrets() error {
q := `select key, value from secrets`
var secrets []Secret var secrets []Secret
err := c.Select(&secrets, q) err := c.store.Find(&secrets, &bh.Query{})
if err != nil { if err != nil {
return err return err
} }
@ -214,36 +202,43 @@ func (c *Config) SetArray(key string, values []string) error {
return c.Set(key, vals) return c.Set(key, vals)
} }
func init() { //func init() {
regex := func(re, s string) (bool, error) { // regex := func(re, s string) (bool, error) {
return regexp.MatchString(re, s) // return regexp.MatchString(re, s)
} // }
sql.Register("sqlite3_custom", // sql.Register("sqlite3_custom",
&sqlite3.SQLiteDriver{ // &sqlite3.SQLiteDriver{
ConnectHook: func(conn *sqlite3.SQLiteConn) error { // ConnectHook: func(conn *sqlite3.SQLiteConn) error {
return conn.RegisterFunc("REGEXP", regex, true) // return conn.RegisterFunc("REGEXP", regex, true)
}, // },
}) // })
} //}
// Readconfig loads the config data out of a JSON file located in cfile // Readconfig loads the config data out of a JSON file located in cfile
func ReadConfig(dbpath string) *Config { func ReadConfig(dbpath string) *Config {
if dbpath == "" { if dbpath == "" {
dbpath = "catbase.db" 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) 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 { if err != nil {
log.Fatal().Err(err) log.Fatal().Err(err).Msgf("could not open sqlite")
} }
c := Config{ c := Config{
DBFile: dbpath, DBFile: dbpath,
secrets: map[string]Secret{}, 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, key string,
value string, value string,
primary key (key) primary key (key)
@ -251,7 +246,7 @@ func ReadConfig(dbpath string) *Config {
log.Fatal().Err(err).Msgf("failed to initialize 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, key string,
value string, value string,
primary key (key) primary key (key)
@ -265,5 +260,60 @@ func ReadConfig(dbpath string) *Config {
log.Info().Msgf("catbase is running.") log.Info().Msgf("catbase is running.")
if err := c.migrate(); err != nil {
log.Fatal().Err(err).Msgf("could not migrate")
}
return &c 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
}

View File

@ -32,7 +32,7 @@ func (c *Config) SetDefaults(mainChannel, nick string) {
} }
var buf bytes.Buffer var buf bytes.Buffer
t.Execute(&buf, vals) t.Execute(&buf, vals)
c.MustExec(`delete from config;`) c.db.MustExec(`delete from config;`)
c.MustExec(buf.String()) c.db.MustExec(buf.String())
log.Info().Msgf("Configuration initialized.") log.Info().Msgf("Configuration initialized.")
} }

6
go.mod
View File

@ -29,7 +29,7 @@ require (
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 // indirect
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // 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/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
github.com/itchyny/gojq v0.12.3 github.com/itchyny/gojq v0.12.3
github.com/james-bowman/nlp v0.0.0-20191016091239-d9dbfaff30c6 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/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852 github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852
github.com/mattn/go-colorable v0.1.6 // indirect 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/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 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/objx v0.2.0 // indirect
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
github.com/temoto/robotstxt v1.1.1 // indirect 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/trubitsyn/go-zero-width v1.0.1
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.1.0 // 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/crypto v0.0.0-20211117183948-ae814b36b871
golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect golang.org/x/exp v0.0.0-20191014171548-69215a2ee97e // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // 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/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gonum.org/v1/gonum v0.6.0 // 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/go-playground/webhooks.v5 v5.13.0
gopkg.in/sourcemap.v1 v1.0.5 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/sqlite v1.14.2
) )

151
go.sum
View File

@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:tP7tkU+vIsEOKiK+l/NSLN4uUtkyuxc6hgYpQeCWAeI=
github.com/dustin/go-jsonpointer v0.0.0-20160814072949-ba0abeacc3dc/go.mod h1:ORH5Qp2bskd9NzSfKqAF7tKfONsEkCarTE5ESr/RVBw= 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= 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/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 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M=
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks= 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/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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 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/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 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= 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= 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-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-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.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.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 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 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E=
github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU= 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= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 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= 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/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 h1:Gh8RCs8ouX3hRSxxK7B1mO5RFByQ4CmJZDwgom++JaA=
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= 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 h1:AAZhtyGXW79T5BouAF0R9FtDhGcp7IGbLZo2Id3N+m8=
github.com/trubitsyn/go-zero-width v1.0.1/go.mod h1:gGhBV4CZHjqXBYSgaxTCKZj+dXJndhdm1zAtAChtIUI= 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= 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/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 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE= 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= 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-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-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-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 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 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= 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/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/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.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-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-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-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-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-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-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-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 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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-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-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-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-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-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-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-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 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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/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.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.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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 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-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-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-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-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-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.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 h1:DJy6UzXbahnGUf1ujUNkh/NEtK14qMo2nvlBPs4U5yw=
gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= 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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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= 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= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -2,11 +2,11 @@ package achievements
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
@ -18,18 +18,14 @@ import (
type AchievementsPlugin struct { type AchievementsPlugin struct {
bot bot.Bot bot bot.Bot
cfg *config.Config cfg *config.Config
db *sqlx.DB store *bh.Store
} }
func New(b bot.Bot) *AchievementsPlugin { func New(b bot.Bot) *AchievementsPlugin {
ap := &AchievementsPlugin{ ap := &AchievementsPlugin{
bot: b, bot: b,
cfg: b.Config(), cfg: b.Config(),
db: b.DB(), store: b.Store(),
}
err := ap.mkDB()
if err != nil {
log.Fatal().Err(err).Msg("unable to create achievements tables")
} }
ap.register() ap.register()
@ -50,41 +46,10 @@ func (p *AchievementsPlugin) register() {
p.bot.Register(p, bot.Help, p.help) 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 { func (p *AchievementsPlugin) GetAwards(nick string) []Award {
var awards []Award var awards []Award
q := `select * from awards inner join trophies on awards.emojy=trophies.emojy where holder=?` err := p.store.Find(&awards, bh.Where("holder").Eq(nick))
if err := p.db.Select(&awards, q, nick); err != nil { if err != nil {
log.Error().Err(err).Msg("could not select awards") log.Error().Err(err).Msg("could not select awards")
} }
return 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 // Award is used by other plugins to register a particular award for a user
func (p *AchievementsPlugin) Grant(nick, emojy string) (Award, error) { func (p *AchievementsPlugin) Grant(nick, emojy string) (Award, error) {
empty := Award{} trophy, err := p.FindTrophy(emojy)
q := `insert into awards (emojy,holder) values (?, ?)`
tx, err := p.db.Beginx()
if err != nil { if err != nil {
return empty, err return Award{}, err
} }
if _, err := tx.Exec(q, emojy, nick); err != nil { award := Award{
tx.Rollback() Holder: nick,
return empty, err Emojy: emojy,
Description: trophy.Description,
Granted: time.Now(),
} }
if err := tx.Commit(); err != nil { if err = p.store.Insert(bh.NextSequence(), &award); err != nil {
return empty, err return Award{}, err
} }
return p.FindAward(emojy)
return award, err
} }
func (p *AchievementsPlugin) Create(emojy, description, creator string) (Trophy, error) { 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) return t, fmt.Errorf("the trophy %s already exists", emojy)
} }
q := `insert into trophies (emojy,description,creator) values (?,?,?)` t = Trophy{
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{
Emojy: emojy, Emojy: emojy,
Description: description, Description: description,
Creator: creator, Creator: creator,
}, err }
err = p.store.Insert(emojy, t)
return t, err
} }
type Trophy struct { type Trophy struct {
@ -253,34 +211,21 @@ type Trophy struct {
} }
type Award struct { type Award struct {
Trophy ID int64 `boltholderid:"ID"`
ID int64
Holder string Holder string
Amount int Emojy string
Granted *time.Time Description string
} Granted time.Time
func (a *Award) Save() error {
return nil
} }
func (p *AchievementsPlugin) AllTrophies() ([]Trophy, error) { func (p *AchievementsPlugin) AllTrophies() ([]Trophy, error) {
q := `select * from trophies order by creator`
var t []Trophy var t []Trophy
err := p.db.Select(&t, q) err := p.store.Find(&t, &bh.Query{})
return t, err return t, err
} }
func (p *AchievementsPlugin) FindTrophy(emojy string) (Trophy, error) { func (p *AchievementsPlugin) FindTrophy(emojy string) (Trophy, error) {
q := `select * from trophies where emojy=?`
var t Trophy var t Trophy
err := p.db.Get(&t, q, emojy) err := p.store.Find(&t, bh.Where("emojy").Eq(emojy))
return t, err 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
}

View File

@ -4,6 +4,7 @@ package admin
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@ -11,8 +12,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -22,7 +21,7 @@ import (
type AdminPlugin struct { type AdminPlugin struct {
bot bot.Bot bot bot.Bot
db *sqlx.DB store *bh.Store
cfg *config.Config cfg *config.Config
quiet bool quiet bool
@ -32,12 +31,10 @@ type AdminPlugin struct {
func New(b bot.Bot) *AdminPlugin { func New(b bot.Bot) *AdminPlugin {
p := &AdminPlugin{ p := &AdminPlugin{
bot: b, bot: b,
db: b.DB(), store: b.Store(),
cfg: b.Config(), cfg: b.Config(),
} }
p.mkDB()
b.RegisterRegex(p, bot.Message, comeBackRegex, p.comeBackCmd) b.RegisterRegex(p, bot.Message, comeBackRegex, p.comeBackCmd)
b.RegisterRegexCmd(p, bot.Message, shutupRegex, p.shutupCmd) b.RegisterRegexCmd(p, bot.Message, shutupRegex, p.shutupCmd)
b.RegisterRegexCmd(p, bot.Message, addBlacklistRegex, p.isAdmin(p.addBlacklistCmd)) b.RegisterRegexCmd(p, bot.Message, addBlacklistRegex, p.isAdmin(p.addBlacklistCmd))
@ -72,16 +69,6 @@ var forbiddenKeys = map[string]bool{
"meme.memes": true, "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 shutupRegex = regexp.MustCompile(`(?i)^shut up$`)
var comeBackRegex = regexp.MustCompile(`(?i)^come back$`) 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 { func (p *AdminPlugin) variableSetCmd(r bot.Request) bool {
variable := strings.ToLower(r.Values["var"]) key := strings.ToLower(r.Values["var"])
value := r.Values["value"] value := r.Values["value"]
variable := bot.Variable{key, value}
var count int64 count, err := p.store.Count(bot.Variable{}, bh.Where("value").Eq(value))
row := p.db.QueryRow(`select count(*) from variables where value = ?`, variable, value)
err := row.Scan(&count)
if err != nil { if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") 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) log.Error().Err(err)
@ -258,7 +244,7 @@ func (p *AdminPlugin) variableSetCmd(r bot.Request) bool {
if count > 0 { if count > 0 {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I've already got that one.") p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I've already got that one.")
} else { } else {
_, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value) err := p.store.Insert(bh.NextSequence(), variable)
if err != nil { if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") 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) log.Error().Err(err)
@ -273,7 +259,8 @@ func (p *AdminPlugin) variableUnSetCmd(r bot.Request) bool {
variable := strings.ToLower(r.Values["var"]) variable := strings.ToLower(r.Values["var"])
value := r.Values["value"] 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 { if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "I'm broke and need attention in my variable creation code.") 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) log.Error().Err(err)
@ -390,9 +377,10 @@ func (p *AdminPlugin) modList(query, channel, plugin string) error {
plugins := p.bot.GetPluginNames() plugins := p.bot.GetPluginNames()
for _, pp := range plugins { for _, pp := range plugins {
if pp == plugin { if pp == plugin {
if _, err := p.db.Exec(query, channel, plugin); err != nil { // todo: yikes
return fmt.Errorf("%w", err) //if _, err := p.db.Exec(query, channel, plugin); err != nil {
} // return fmt.Errorf("%w", err)
//}
err := p.bot.RefreshPluginWhitelist() err := p.bot.RefreshPluginWhitelist()
if err != nil { if err != nil {
return fmt.Errorf("%w", err) return fmt.Errorf("%w", err)

View File

@ -6,6 +6,8 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"github.com/velour/catbase/config"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
@ -106,11 +108,18 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
} }
req.PassEntry.Pass = fmt.Sprintf("%x", md5.Sum(b)) req.PassEntry.Pass = fmt.Sprintf("%x", md5.Sum(b))
} }
q := `insert into apppass (secret, encoded_pass, cost) values (?, ?, ?)`
req.PassEntry.EncodePass() req.PassEntry.EncodePass()
check := bcrypt.CompareHashAndPassword(req.PassEntry.encodedPass, []byte(req.PassEntry.Pass)) 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(). log.Debug().
Str("secret", req.PassEntry.Secret). Str("secret", req.PassEntry.Secret).
Str("encoded", string(req.PassEntry.encodedPass)). Str("encoded", string(req.PassEntry.encodedPass)).
@ -118,17 +127,11 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
Interface("check", check). Interface("check", check).
Msg("debug pass creation") 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 { if err != nil {
writeErr(w, err) writeErr(w, err)
return return
} }
id, err := res.LastInsertId()
if err != nil {
writeErr(w, err)
return
}
req.PassEntry.ID = id
j, _ := json.Marshal(req.PassEntry) j, _ := json.Marshal(req.PassEntry)
fmt.Fprint(w, string(j)) fmt.Fprint(w, string(j))
return return
@ -137,16 +140,14 @@ func (p *AdminPlugin) handleAppPassAPI(w http.ResponseWriter, r *http.Request) {
writeErr(w, fmt.Errorf("missing ID")) writeErr(w, fmt.Errorf("missing ID"))
return return
} }
q := `delete from apppass where id = ?` err := p.store.Delete(req.PassEntry.ID, PassEntry{})
_, err := p.db.Exec(q, req.PassEntry.ID)
if err != nil { if err != nil {
writeErr(w, err) writeErr(w, err)
return return
} }
} }
q := `select id,secret from apppass where secret = ?`
passEntries := []PassEntry{} 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 { if err != nil {
writeErr(w, err) writeErr(w, err)
return 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) { func (p *AdminPlugin) handleVarsAPI(w http.ResponseWriter, r *http.Request) {
var configEntries []struct { configEntries := []config.Value{}
Key string `json:"key"` err := p.store.Find(&configEntries, &bh.Query{})
Value string `json:"value"`
}
q := `select key, value from config`
err := p.db.Select(&configEntries, q)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).

View File

@ -6,13 +6,14 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"github.com/velour/catbase/plugins/remember"
"math/rand" "math/rand"
"regexp" "regexp"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -25,74 +26,65 @@ var (
type BabblerPlugin struct { type BabblerPlugin struct {
Bot bot.Bot Bot bot.Bot
db *sqlx.DB store *bh.Store
WithGoRoutines bool WithGoRoutines bool
handlers bot.HandlerTable handlers bot.HandlerTable
} }
type Babbler struct { type Babbler struct {
BabblerId int64 `db:"id"` BabblerId int64 `db:"id" boltholdid:"BabblerId"`
Name string `db:"babbler"` 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 { type BabblerWord struct {
WordId int64 `db:"id"` WordId int64 `db:"id" boltholdid:"WordId"`
Word string `db:"word"` 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 { type BabblerNode struct {
NodeId int64 `db:"id"` NodeId int64 `db:"id" boltholdid:"NodeId"`
BabblerId int64 `db:"babblerId"` BabblerId int64 `db:"babblerId"`
WordId int64 `db:"wordId"` WordId int64 `db:"wordId"`
Root int64 `db:"root"` Root int64 `db:"root"`
RootFrequency int64 `db:"rootFrequency"` 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 { type BabblerArc struct {
ArcId int64 `db:"id"` ArcId int64 `db:"id" boltholdid:"ArcId"`
FromNodeId int64 `db:"fromNodeId"` FromNodeId int64 `db:"fromNodeId"`
ToNodeId int64 `db:"toNodeId"` ToNodeId int64 `db:"toNodeId"`
Frequency int64 `db:"frequency"` 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 { 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{ plugin := &BabblerPlugin{
Bot: b, Bot: b,
db: b.DB(), store: b.Store(),
WithGoRoutines: true, 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) { func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) {
res, err := p.db.Exec(`insert into babblers (babbler) values (?);`, name) b := &Babbler{
if err == nil { Name: name,
id, err := res.LastInsertId() }
err := p.store.Insert(bh.NextSequence(), b)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, err return nil, err
} }
return &Babbler{ return b, err
BabblerId: id,
Name: name,
}, nil
}
return nil, err
} }
func (p *BabblerPlugin) getBabbler(name string) (*Babbler, error) { func (p *BabblerPlugin) getBabbler(name string) (*Babbler, error) {
var bblr Babbler 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 != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Error().Msg("failed to find babbler") log.Error().Msg("failed to find babbler")
@ -233,29 +221,9 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
return nil, err return nil, err
} }
rows, err := p.db.Queryx(fmt.Sprintf("select tidbit from factoid where fact like '%s quotes';", babbler.Name)) quotes := remember.AllQuotesFrom(p.store, babbler.Name)
if err != nil { for _, q := range quotes {
log.Error().Err(err) if err = p.addToMarkovChain(babbler, q.Tidbit); err != nil {
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 {
log.Error().Err(err) log.Error().Err(err)
} }
} }
@ -265,9 +233,9 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) { func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) {
var w BabblerWord 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 != nil {
if err == sql.ErrNoRows { if err == bh.ErrNotFound {
return nil, NEVER_SAID return nil, NEVER_SAID
} }
return nil, err return nil, err
@ -276,20 +244,13 @@ func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) {
} }
func (p *BabblerPlugin) createNewWord(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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, err return nil, err
} }
id, err := res.LastInsertId() return w, nil
if err != nil {
log.Error().Err(err)
return nil, err
}
return &BabblerWord{
WordId: id,
Word: word,
}, nil
} }
func (p *BabblerPlugin) getOrCreateWord(word string) (*BabblerWord, error) { func (p *BabblerPlugin) getOrCreateWord(word string) (*BabblerWord, error) {
@ -310,9 +271,9 @@ func (p *BabblerPlugin) getBabblerNode(babbler *Babbler, word string) (*BabblerN
} }
var node BabblerNode 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 != nil {
if err == sql.ErrNoRows { if err == bh.ErrNotFound {
return nil, NEVER_SAID return nil, NEVER_SAID
} }
return nil, err return nil, err
@ -327,24 +288,19 @@ func (p *BabblerPlugin) createBabblerNode(babbler *Babbler, word string) (*Babbl
return nil, err return nil, err
} }
res, err := p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?, ?, 0, 0)`, babbler.BabblerId, w.WordId) bn := &BabblerNode{
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,
WordId: w.WordId, WordId: w.WordId,
Root: 0, Root: 0,
RootFrequency: 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) { 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) log.Error().Err(err)
return nil, 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, 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) { func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerArc, error) {
var arc BabblerArc 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 != nil {
if err == sql.ErrNoRows { if err == bh.ErrNotFound {
return nil, NEVER_SAID return nil, NEVER_SAID
} }
return nil, err return nil, err
@ -383,24 +344,32 @@ func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerAr
} }
func (p *BabblerPlugin) incrementWordArc(fromNode, toNode *BabblerNode) (*BabblerArc, error) { 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, err return nil, err
} }
affectedRows := int64(0)
if err == nil {
affectedRows, _ = res.RowsAffected()
}
if affectedRows == 0 { 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, err return nil, err
} }
} }
return p.getBabblerArc(fromNode, toNode) 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) { 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
defer rows.Close()
rootNodes := []*BabblerNode{}
total := int64(0) total := int64(0)
for rows.Next() { for _, n := range rootNodes {
var node BabblerNode total += n.RootFrequency
err = rows.StructScan(&node)
if err != nil {
log.Error().Err(err)
return nil, nil, err
}
rootNodes = append(rootNodes, &node)
total += node.RootFrequency
} }
if len(rootNodes) == 0 { if len(rootNodes) == 0 {
@ -474,13 +435,12 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba
for _, node := range rootNodes { for _, node := range rootNodes {
total += node.RootFrequency total += node.RootFrequency
if total >= which { if total >= which {
var w BabblerWord w, err := getWord(p.store, node.WordId)
err := p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, 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) { 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
defer rows.Close()
arcs := []*BabblerArc{}
total := int64(0) total := int64(0)
for rows.Next() { for _, a := range arcs {
var arc BabblerArc total += a.Frequency
err = rows.StructScan(&arc)
if err != nil {
log.Error().Err(err)
return nil, nil, err
}
arcs = append(arcs, &arc)
total += arc.Frequency
} }
if len(arcs) == 0 { if len(arcs) == 0 {
@ -520,20 +471,18 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode
total += arc.Frequency total += arc.Frequency
if total >= which { if total >= which {
var node BabblerNode node, err := getNode(p.store, arc.ToNodeId)
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.ToNodeId).StructScan(&node)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
var w BabblerWord w, err := getWord(p.store, node.WordId)
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, 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) { func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNode, *BabblerWord, bool, error) {
rows, err := p.db.Queryx(`select * from babblerArcs where toNodeId = ?;`, toNode.NodeId)
if err != nil {
log.Error().Err(err)
return nil, nil, false, err
}
defer rows.Close()
arcs := []*BabblerArc{} arcs := []*BabblerArc{}
total := int64(0) err := p.store.Find(&arcs, bh.Where("toNodeId").Eq(toNode.NodeId))
for rows.Next() {
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
arcs = append(arcs, &arc)
total := int64(0)
for _, arc := range arcs {
total += arc.Frequency total += arc.Frequency
} }
@ -575,24 +516,21 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo
total = 0 total = 0
for _, arc := range arcs { for _, arc := range arcs {
total += arc.Frequency total += arc.Frequency
if total >= which { if total >= which {
var node BabblerNode node, err := getNode(p.store, arc.FromNodeId)
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.FromNodeId).StructScan(&node)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
var w BabblerWord w, err := getWord(p.store, node.WordId)
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, nil, false, 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") 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{} mapping := map[int64]*BabblerNode{}
rows, err := p.db.Queryx("select * from babblerNodes where babblerId = ?;", otherBabbler.BabblerId)
if err != nil {
log.Error().Err(err)
return err
}
defer rows.Close()
nodes := []*BabblerNode{} nodes := []*BabblerNode{}
err = p.store.Find(&nodes, bh.Where("babblerId").Eq(otherBabbler.BabblerId))
for rows.Next() {
var node BabblerNode
err = rows.StructScan(&node)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return err return err
} }
nodes = append(nodes, &node)
}
for _, node := range nodes { for _, node := range nodes {
var res sql.Result
if node.NodeId == otherNode.NodeId { if node.NodeId == otherNode.NodeId {
node.WordId = intoNode.WordId node.WordId = intoNode.WordId
} }
affected := 0
if node.Root > 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
} }
} else { } 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
} }
} }
rowsAffected := int64(-1) if err != nil || affected == 0 {
if err == nil { node.BabblerId = intoBabbler.BabblerId
rowsAffected, _ = res.RowsAffected() err = p.store.Insert(bh.NextSequence(), &node)
}
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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return err return err
@ -738,7 +674,8 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
} }
var updatedNode BabblerNode 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return err return err
@ -748,23 +685,11 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
} }
for oldNodeId, newNode := range mapping { for oldNodeId, newNode := range mapping {
rows, err := p.db.Queryx("select * from babblerArcs where fromNodeId = ?;", oldNodeId)
if err != nil {
return err
}
defer rows.Close()
arcs := []*BabblerArc{} arcs := []*BabblerArc{}
err = p.store.Find(&arcs, bh.Where("fromNodeId").Eq(oldNodeId))
for rows.Next() {
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Error().Err(err)
return err return err
} }
arcs = append(arcs, &arc)
}
for _, arc := range arcs { for _, arc := range arcs {
_, err := p.incrementWordArc(newNode, mapping[arc.ToNodeId]) _, 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) { func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error) {
arcs := []*BabblerArc{} 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return arcs, 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 return arcs, nil
} }
func (p *BabblerPlugin) getBabblerNodeById(nodeId int64) (*BabblerNode, error) { func (p *BabblerPlugin) getBabblerNodeById(nodeId int64) (*BabblerNode, error) {
var node BabblerNode node, err := getNode(p.store, nodeId)
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, nodeId).StructScan(&node)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil, err return nil, err
} }
return &node, nil return node, nil
} }
func shuffle(a []*BabblerArc) { func shuffle(a []*BabblerArc) {
@ -932,8 +845,7 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
log.Error().Err(err) log.Error().Err(err)
return "", err return "", err
} }
var w BabblerWord w, err := getWord(p.store, cur.WordId)
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, cur.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return "", err return "", err

View File

@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"image" "image"
"image/png" "image/png"
"io/ioutil" "io/ioutil"
@ -21,7 +22,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/nfnt/resize" "github.com/nfnt/resize"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -31,8 +31,6 @@ import (
"github.com/velour/catbase/plugins/counter" "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{} var cachedImages = map[string][]byte{}
const DEFAULT_ITEM = "🍺" const DEFAULT_ITEM = "🍺"
@ -40,15 +38,14 @@ const DEFAULT_ITEM = "🍺"
type BeersPlugin struct { type BeersPlugin struct {
b bot.Bot b bot.Bot
c *config.Config c *config.Config
db *sqlx.DB store *bh.Store
untapdCache map[int]bool untapdCache map[int]bool
handlers bot.HandlerTable handlers bot.HandlerTable
} }
type untappdUser struct { type untappdUser struct {
id int64 untappdUser string `boltholdid:"untappdUser"`
untappdUser string
channel string channel string
lastCheckin int lastCheckin int
chanNick string chanNick string
@ -56,19 +53,10 @@ type untappdUser struct {
// New BeersPlugin creates a new BeersPlugin with the Plugin interface // New BeersPlugin creates a new BeersPlugin with the Plugin interface
func New(b bot.Bot) *BeersPlugin { 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{ p := &BeersPlugin{
b: b, b: b,
c: b.Config(), c: b.Config(),
db: b.DB(), store: b.Store(),
untapdCache: make(map[int]bool), untapdCache: make(map[int]bool),
} }
@ -182,9 +170,7 @@ func (p *BeersPlugin) register() {
Str("nick", u.chanNick). Str("nick", u.chanNick).
Msg("Creating Untappd user") Msg("Creating Untappd user")
var count int count, err := p.store.Count(untappdUser{}, bh.Where("untappdUser").Eq(u.untappdUser))
err := p.db.QueryRow(`select count(*) from untappd
where untappdUser = ?`, u.untappdUser).Scan(&count)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Error registering untappd") 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.") p.b.Send(r.Conn, bot.Message, channel, "I'm already watching you.")
return true return true
} }
_, err = p.db.Exec(`insert into untappd ( err = p.store.Insert(u.untappdUser, u)
untappdUser,
channel,
lastCheckin,
chanNick
) values (?, ?, ?, ?);`,
u.untappdUser,
u.channel,
0,
u.chanNick,
)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Error registering untappd") log.Error().Err(err).Msgf("Error registering untappd")
p.b.Send(r.Conn, bot.Message, channel, "I can't see.") 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 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 // TODO: really ought to have an ID here
booze, _ := counter.GetUserItem(db, user, id, itemName) booze, _ := counter.GetUserItem(store, user, id, itemName)
return booze return booze
} }
func (p *BeersPlugin) setBeers(r *bot.Request, user, id string, amount int) { func (p *BeersPlugin) setBeers(r *bot.Request, user, id string, amount int) {
itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) 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) err := ub.Update(r, amount)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Error saving beers") 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) { func (p *BeersPlugin) addBeers(r *bot.Request, user, id string, delta int) {
itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) 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) err := ub.UpdateDelta(r, delta)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Error saving beers") 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 { func (p *BeersPlugin) getBeers(user, id string) int {
itemName := p.c.Get("beers.itemname", DEFAULT_ITEM) 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 return ub.Count
} }
@ -401,17 +377,13 @@ func (p *BeersPlugin) checkUntappd(c bot.Connector, channel string) {
} }
userMap := make(map[string]untappdUser) 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 { if err != nil {
log.Error().Err(err).Msg("Error getting untappd users") log.Error().Err(err).Msg("Error getting untappd users")
return return
} }
for rows.Next() { for _, u := range users {
u := untappdUser{}
err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick)
if err != nil {
log.Fatal().Err(err)
}
userMap[u.untappdUser] = u userMap[u.untappdUser] = u
if u.chanNick == "" { if u.chanNick == "" {
log.Fatal().Msg("Empty chanNick for no good reason.") 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...) args = append([]interface{}{channel, msg}, args...)
user.lastCheckin = checkin.Checkin_id user.lastCheckin = checkin.Checkin_id
_, err := p.db.Exec(`update untappd set
lastCheckin = ? err := p.store.Update(user.untappdUser, user)
where id = ?`, user.lastCheckin, user.id)
if err != nil { if err != nil {
log.Error().Err(err).Msg("UPDATE ERROR!") log.Error().Err(err).Msg("UPDATE ERROR!")
} }

View File

@ -3,9 +3,9 @@ package countdown
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
@ -17,14 +17,14 @@ var thisYear = time.Now().Year()
type CountdownPlugin struct { type CountdownPlugin struct {
b bot.Bot b bot.Bot
db *sqlx.DB store *bh.Store
c *config.Config c *config.Config
} }
func New(b bot.Bot) *CountdownPlugin { func New(b bot.Bot) *CountdownPlugin {
p := &CountdownPlugin{ p := &CountdownPlugin{
b: b, b: b,
db: b.DB(), store: b.Store(),
c: b.Config(), c: b.Config(),
} }
@ -48,27 +48,28 @@ func (p *CountdownPlugin) scheduleNYE(f func()) {
func (p *CountdownPlugin) newYearRollover() { func (p *CountdownPlugin) newYearRollover() {
log.Debug().Msgf("Happy new year!") log.Debug().Msgf("Happy new year!")
tx, err := p.db.Beginx() // todo: something about rolling over
if err != nil { //tx, err := p.db.Beginx()
logError(err, "error getting transaction") //if err != nil {
return // logError(err, "error getting transaction")
} // return
q := fmt.Sprintf(`create table counter_%d as select * from counter`, thisYear) //}
_, err = tx.Exec(q) //q := fmt.Sprintf(`create table counter_%d as select * from counter`, thisYear)
if err != nil { //_, err = tx.Exec(q)
logError(err, "error running insert into") //if err != nil {
logError(tx.Rollback(), "error with insert rollback") // logError(err, "error running insert into")
return // logError(tx.Rollback(), "error with insert rollback")
} // return
q = `delete from counter` //}
_, err = tx.Exec(q) //q = `delete from counter`
if err != nil { //_, err = tx.Exec(q)
logError(err, "error running delete") //if err != nil {
logError(tx.Rollback(), "error with delete rollback") // logError(err, "error running delete")
return // logError(tx.Rollback(), "error with delete rollback")
} // return
err = tx.Commit() //}
logError(err, "error committing transaction") //err = tx.Commit()
//logError(err, "error committing transaction")
} }
func logError(err error, msg string) { func logError(err error, msg string) {

View File

@ -59,7 +59,7 @@ func (p *CounterPlugin) mkIncrementAPI(delta int) func(w http.ResponseWriter, r
return return
} }
item, err := GetUserItem(p.db, userName, u.ID, itemName) item, err := GetUserItem(p.store, userName, u.ID, itemName)
if err != nil { if err != nil {
log.Error().Err(err).Msg("error finding item") log.Error().Err(err).Msg("error finding item")
w.WriteHeader(400) w.WriteHeader(400)
@ -136,7 +136,7 @@ func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request)
return return
} }
nick, id := p.resolveUser(bot.Request{Conn: p.b.DefaultConnector()}, info.User) 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 { if err != nil {
log.Error(). log.Error().
Err(err). 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 { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, err) fmt.Fprint(w, err)

View File

@ -3,6 +3,7 @@ package counter
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"math/rand" "math/rand"
"regexp" "regexp"
"strconv" "strconv"
@ -11,8 +12,6 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -21,12 +20,12 @@ import (
type CounterPlugin struct { type CounterPlugin struct {
b bot.Bot b bot.Bot
db *sqlx.DB store *bh.Store
cfg *config.Config cfg *config.Config
} }
type Item struct { type Item struct {
*sqlx.DB *bh.Store
ID int64 ID int64
Nick string Nick string
@ -36,7 +35,7 @@ type Item struct {
} }
type alias struct { type alias struct {
*sqlx.DB *bh.Store
ID int64 ID int64
Item string Item string
@ -44,116 +43,97 @@ type alias struct {
} }
// GetItems returns all counters // GetItems returns all counters
func GetAllItems(db *sqlx.DB) ([]Item, error) { func GetAllItems(store *bh.Store) ([]Item, error) {
var items []Item var items []Item
err := db.Select(&items, `select * from counter`) err := store.Find(&items, &bh.Query{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Don't forget to embed the db into all of that shiz // Don't forget to embed the db into all of that shiz
for i := range items { for i := range items {
items[i].DB = db items[i].Store = store
} }
return items, nil return items, nil
} }
// GetItems returns all counters for a subject // 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 items []Item
var err error var err error
q := &bh.Query{}
if id != "" { if id != "" {
err = db.Select(&items, `select * from counter where userid = ?`, id) q = bh.Where("userid").Eq(id)
} else { } 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 return nil, err
} }
// Don't forget to embed the db into all of that shiz // Don't forget to embed the db into all of that shiz
for i := range items { for i := range items {
items[i].DB = db items[i].Store = store
} }
return items, nil return items, nil
} }
func LeaderAll(db *sqlx.DB) ([]Item, error) { 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` //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 var items []Item
err := db.Select(&items, s) err := store.Find(&items, &bh.Query{})
if err != nil { if err != nil {
log.Error().Msgf("Error querying leaderboard: %s", err) log.Error().Msgf("Error querying leaderboard: %s", err)
return nil, err return nil, err
} }
for i := range items { for i := range items {
items[i].DB = db items[i].Store = store
} }
return items, nil 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) 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 var items []Item
err := db.Select(&items, s, itemName) err := store.Find(&items, bh.Where("item").Eq(itemName).SortBy("count").Reverse())
if err != nil { if err != nil {
return nil, err return nil, err
} }
for i := range items { for i := range items {
items[i].DB = db items[i].Store = store
} }
return items, nil return items, nil
} }
func RmAlias(db *sqlx.DB, aliasName string) error { func RmAlias(store *bh.Store, aliasName string) error {
q := `delete from counter_alias where item = ?` return store.Delete(alias{}, aliasName)
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 MkAlias(db *sqlx.DB, aliasName, pointsTo string) (*alias, error) { func MkAlias(store *bh.Store, aliasName, pointsTo string) (*alias, error) {
aliasName = strings.ToLower(aliasName) aliasName = strings.ToLower(aliasName)
pointsTo = strings.ToLower(pointsTo) pointsTo = strings.ToLower(pointsTo)
res, err := db.Exec(`insert into counter_alias (item, points_to) values (?, ?)`, alias := &alias{
aliasName, pointsTo) Store: store,
if err != nil { Item: aliasName,
_, err := db.Exec(`update counter_alias set points_to=? where item=?`, pointsTo, aliasName) PointsTo: pointsTo,
if err != nil {
return nil, err
} }
var a alias err := store.Insert(bh.NextSequence(), alias)
if err := db.Get(&a, `select * from counter_alias where item=?`, aliasName); err != nil { return alias, err
return nil, err
}
return &a, nil
}
id, _ := res.LastInsertId()
return &alias{db, id, aliasName, pointsTo}, nil
} }
// GetUserItem returns a specific counter for all subjects // 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) itemName = trimUnicode(itemName)
var items []Item var items []Item
var a alias 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 itemName = a.PointsTo
} else { } else {
log.Error().Err(err).Interface("alias", a) 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 { if err != nil {
return nil, err return nil, err
} }
@ -162,37 +142,30 @@ func GetItem(db *sqlx.DB, itemName string) ([]Item, error) {
Interface("items", items). Interface("items", items).
Msg("got item") Msg("got item")
for _, i := range items { for _, i := range items {
i.DB = db i.Store = store
} }
return items, nil return items, nil
} }
// GetUserItem returns a specific counter for a subject // 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) itemName = trimUnicode(itemName)
var item Item var item Item
item.DB = db item.Store = store
var a alias 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 itemName = a.PointsTo
} else { } else {
log.Error().Err(err).Interface("alias", a) log.Error().Err(err).Interface("alias", a)
} }
var err error var err error
q := bh.Where("nick").Eq(nick).And("item").Eq(itemName)
if id != "" { if id != "" {
err = db.Get(&item, `select * from counter where userid = ? and item= ?`, id, itemName) q = bh.Where("userid").Eq(id).And("item").Eq(itemName)
} else {
err = db.Get(&item, `select * from counter where nick = ? and item= ?`, nick, itemName)
} }
switch err { err = store.FindOne(&item, q)
case sql.ErrNoRows: if err != nil {
item.ID = -1
item.Nick = nick
item.Item = itemName
item.UserID = id
case nil:
default:
return Item{}, err return Item{}, err
} }
log.Debug(). 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 // GetUserItem returns a specific counter for a subject
// Create saves a counter // Create saves a counter
func (i *Item) Create() error { func (i *Item) Create() error {
res, err := i.Exec(`insert into counter (nick, item, count, userid) values (?, ?, ?, ?);`, err := i.Store.Insert(bh.NextSequence(), i)
i.Nick, i.Item, i.Count, i.UserID)
if err != nil {
return err
}
id, _ := res.LastInsertId()
// hackhackhack?
i.ID = id
return err return err
} }
@ -232,7 +198,7 @@ func (i *Item) Update(r *bot.Request, value int) error {
Interface("i", i). Interface("i", i).
Int("value", value). Int("value", value).
Msg("Updating item") 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 { if err == nil {
sendUpdate(r, i.Nick, i.Item, i.Count) sendUpdate(r, i.Nick, i.Item, i.Count)
} }
@ -248,81 +214,48 @@ func (i *Item) UpdateDelta(r *bot.Request, delta int) error {
// Delete removes a counter from the database // Delete removes a counter from the database
func (i *Item) Delete() error { 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 i.ID = -1
return err return err
} }
func (p *CounterPlugin) migrate(r bot.Request) bool { func (p *CounterPlugin) migrate(r bot.Request) bool {
db := p.db // 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`) //nicks := []string{}
if err != nil { //err := db.Select(&nicks, `select distinct nick from counter where userid is null`)
log.Error().Err(err).Msg("could not get nick list") //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 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 // NewCounterPlugin creates a new CounterPlugin with the Plugin interface
func New(b bot.Bot) *CounterPlugin { func New(b bot.Bot) *CounterPlugin {
if err := setupDB(b); err != nil {
panic(err)
}
cp := &CounterPlugin{ cp := &CounterPlugin{
b: b, b: b,
db: b.DB(), store: b.Store(),
cfg: b.Config(), cfg: b.Config(),
} }
@ -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)) p.b.Send(r.Conn, bot.Message, fmt.Sprintf("You must provide all fields for an alias: %s", mkAliasRegex))
return true 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") 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.") 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 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.") p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "You must specify an alias to remove.")
return true 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") 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.") p.b.Send(r.Conn, bot.Message, r.Msg.Channel, "`sudo rm your mom` => Nope, she's staying with me.")
return true return true
@ -407,10 +340,10 @@ func (p *CounterPlugin) leaderboardCmd(r bot.Request) bool {
what := r.Values["what"] what := r.Values["what"]
if what == "" { if what == "" {
cmd = func() ([]Item, error) { return LeaderAll(p.db) } cmd = func() ([]Item, error) { return LeaderAll(p.store) }
} else { } else {
itNameTxt = fmt.Sprintf(" for %s", what) 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() its, err := cmd()
@ -438,7 +371,7 @@ func (p *CounterPlugin) resetCmd(r bot.Request) bool {
nick, id := p.resolveUser(r, "") nick, id := p.resolveUser(r, "")
channel := r.Msg.Channel channel := r.Msg.Channel
items, err := GetItems(p.db, nick, id) items, err := GetItems(p.store, nick, id)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -472,7 +405,7 @@ func (p *CounterPlugin) inspectCmd(r bot.Request) bool {
Str("id", id). Str("id", id).
Msg("Getting counter") Msg("Getting counter")
// pull all of the items associated with "subject" // 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 { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -513,7 +446,7 @@ func (p *CounterPlugin) clearCmd(r bot.Request) bool {
channel := r.Msg.Channel channel := r.Msg.Channel
c := r.Conn c := r.Conn
it, err := GetUserItem(p.db, nick, id, itemName) it, err := GetUserItem(p.store, nick, id, itemName)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -551,7 +484,7 @@ func (p *CounterPlugin) countCmd(r bot.Request) bool {
} }
var item Item var item Item
item, err := GetUserItem(p.db, nick, id, itemName) item, err := GetUserItem(p.store, nick, id, itemName)
switch { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, fmt.Sprintf("I don't think %s has any %s.", 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"] itemName := r.Values["thing"]
channel := r.Msg.Channel channel := r.Msg.Channel
// ++ those fuckers // ++ those fuckers
item, err := GetUserItem(p.db, nick, id, itemName) item, err := GetUserItem(p.store, nick, id, itemName)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -606,7 +539,7 @@ func (p *CounterPlugin) decrementCmd(r bot.Request) bool {
itemName := r.Values["thing"] itemName := r.Values["thing"]
channel := r.Msg.Channel channel := r.Msg.Channel
// -- those fuckers // -- those fuckers
item, err := GetUserItem(p.db, nick, id, itemName) item, err := GetUserItem(p.store, nick, id, itemName)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -628,7 +561,7 @@ func (p *CounterPlugin) addToCmd(r bot.Request) bool {
itemName := r.Values["thing"] itemName := r.Values["thing"]
channel := r.Msg.Channel channel := r.Msg.Channel
// += those fuckers // += those fuckers
item, err := GetUserItem(p.db, nick, id, itemName) item, err := GetUserItem(p.store, nick, id, itemName)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -652,7 +585,7 @@ func (p *CounterPlugin) removeFromCmd(r bot.Request) bool {
itemName := r.Values["thing"] itemName := r.Values["thing"]
channel := r.Msg.Channel channel := r.Msg.Channel
// -= those fuckers // -= those fuckers
item, err := GetUserItem(p.db, nick, id, itemName) item, err := GetUserItem(p.store, nick, id, itemName)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -693,7 +626,7 @@ func (p *CounterPlugin) teaMatchCmd(r bot.Request) bool {
itemName := strings.ToLower(submatches[1]) itemName := strings.ToLower(submatches[1])
// We will specifically allow :tea: to keep compatability // 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:") { if err != nil || (item.Count == 0 && item.Item != ":tea:") {
log.Error(). log.Error().
Err(err). Err(err).

View File

@ -3,10 +3,10 @@
package fact package fact
import ( import (
"database/sql"
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"html/template" "html/template"
"math/rand" "math/rand"
"net/http" "net/http"
@ -18,8 +18,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "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 // Factoid stores info about our factoid for lookup and later interaction
type Factoid struct { type Factoid struct {
ID sql.NullInt64 ID int64 `boltholdid:"ID"`
Fact string Fact string
Tidbit string Tidbit string
Verb string Verb string
@ -47,47 +45,50 @@ type alias struct {
Next string 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 // 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 var next alias
err := db.Get(&next, q, a.Next) err := store.FindOne(&next, bh.Where("fact").Eq(a.Next))
if err != nil { if err != nil {
// we hit the end of the chain, get a factoid named Next // 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 { if err != nil {
err := fmt.Errorf("Error resolvig alias %v: %v", a, err) err := fmt.Errorf("Error resolvig alias %v: %v", a, err)
return nil, err return nil, err
} }
return fact, nil return fact, nil
} }
return next.resolve(db) return next.resolve(store)
} }
func findAlias(db *sqlx.DB, fact string) (bool, *Factoid) { func findAlias(store *bh.Store, fact string) (bool, *Factoid) {
q := `select * from factoid_alias where fact=?` // todo: remove this query
//q := `select * from factoid_alias where fact=?`
var a alias var a alias
err := db.Get(&a, q, fact) err := store.FindOne(&a, bh.Where("fact").Eq(fact))
if err != nil { if err != nil {
return false, nil return false, nil
} }
f, err := a.resolve(db) f, err := a.resolve(store)
return err == nil, f return err == nil, f
} }
func (a *alias) save(db *sqlx.DB) error { func (a *alias) save(store *bh.Store) error {
q := `select * from factoid_alias where fact=?` //q := `select * from factoid_alias where fact=?`
var offender alias var offender alias
err := db.Get(&offender, q, a.Next) err := store.FindOne(&offender, bh.Where("fact").Eq(a.Next))
if err == nil { if err == nil {
return fmt.Errorf("DANGER: an opposite alias already exists") return fmt.Errorf("DANGER: an opposite alias already exists")
} }
_, err = a.resolve(db) _, err = a.resolve(store)
if err != nil { if err != nil {
return fmt.Errorf("there is no fact at that destination") return fmt.Errorf("there is no fact at that destination")
} }
q = `insert or replace into factoid_alias (fact, next) values (?, ?)` err = store.Upsert(a.Fact, a)
_, err = db.Exec(q, a.Fact, a.Next) //q = `insert or replace into factoid_alias (fact, next) values (?, ?)`
// todo: remove query
if err != nil { if err != nil {
return err return err
} }
@ -98,168 +99,54 @@ func aliasFromStrings(from, to string) *alias {
return &alias{from, to} return &alias{from, to}
} }
func (f *Factoid) Save(db *sqlx.DB) error { func (f *Factoid) Save(store *bh.Store) error {
var err error var err error
if f.ID.Valid { if f.ID != 0 {
// update f.Accessed = time.Now()
_, err = db.Exec(`update factoid set err = store.Update(f.ID, f)
fact=?,
tidbit=?,
verb=?,
owner=?,
accessed=?,
count=?
where id=?`,
f.Fact,
f.Tidbit,
f.Verb,
f.Owner,
f.Accessed.Unix(),
f.Count,
f.ID.Int64)
} else { } else {
f.Created = time.Now() f.Created = time.Now()
f.Accessed = time.Now() f.Accessed = time.Now()
// insert err = store.Insert(bh.NextSequence(), f)
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
} }
return err return err
} }
func (f *Factoid) delete(db *sqlx.DB) error { func (f *Factoid) delete(store *bh.Store) error {
var err error var err error
if f.ID.Valid { if f.ID != 0 {
_, err = db.Exec(`delete from factoid where id=?`, f.ID) err = store.Delete(f.ID, Factoid{})
} }
f.ID.Valid = false f.ID = 0
return err 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 var fs []*Factoid
query := `select err := store.Find(&fs, bh.Where("fact").Contains(fact).And("tidbit").Contains(tidbit))
id,
fact,
tidbit,
verb,
owner,
created,
accessed,
count
from factoid
where fact like ?
and tidbit like ?;`
rows, err := db.Query(query,
"%"+fact+"%", "%"+tidbit+"%")
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error regexping for facts") log.Error().Err(err).Msg("Error regexping for facts")
return nil, err 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 return fs, err
} }
func GetSingle(db *sqlx.DB) (*Factoid, error) { func GetSingle(store *bh.Store) (*Factoid, error) {
var f Factoid var allMatching []Factoid
var tmpCreated int64 if err := store.Find(&allMatching, &bh.Query{}); err != nil {
var tmpAccessed int64 return nil, err
err := db.QueryRow(`select }
id, f := allMatching[rand.Intn(len(allMatching))]
fact, return &f, nil
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 GetSingleFact(db *sqlx.DB, fact string) (*Factoid, error) { func GetSingleFact(store *bh.Store, fact string) (*Factoid, error) {
var f Factoid var allMatching []Factoid
var tmpCreated int64 if err := store.Find(&allMatching, bh.Where("fact").Contains(fact)); err != nil {
var tmpAccessed int64 return nil, err
err := db.QueryRow(`select }
id, f := allMatching[rand.Intn(len(allMatching))]
fact, return &f, nil
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
} }
// Factoid provides the necessary plugin-wide needs // Factoid provides the necessary plugin-wide needs
@ -267,7 +154,7 @@ type FactoidPlugin struct {
Bot bot.Bot Bot bot.Bot
NotFound []string NotFound []string
LastFact *Factoid LastFact *Factoid
db *sqlx.DB store *bh.Store
handlers bot.HandlerTable handlers bot.HandlerTable
} }
@ -283,32 +170,11 @@ func New(botInst bot.Bot) *FactoidPlugin {
"NOPE! NOPE! NOPE!", "NOPE! NOPE! NOPE!",
"One time, I learned how to jump rope.", "One time, I learned how to jump rope.",
}, },
db: botInst.DB(), store: botInst.Store(),
} }
c := botInst.DefaultConnector() 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{}) { for _, channel := range botInst.Config().GetArray("channels", []string{}) {
go p.factTimer(c, channel) go p.factTimer(c, channel)
@ -366,14 +232,11 @@ func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string
} }
} }
var count sql.NullInt64 count, err := p.store.Count(Factoid{}, bh.Where("fact").Eq(fact).And("verb").Eq(verb).And("tidbit").Eq(tidbit))
err := p.db.QueryRow(`select count(*) from factoid
where fact=? and verb=? and tidbit=?`,
fact, verb, tidbit).Scan(&count)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error counting facts") log.Error().Err(err).Msg("Error counting facts")
return fmt.Errorf("What?") return fmt.Errorf("What?")
} else if count.Valid && count.Int64 != 0 { } else if count != 0 {
log.Debug().Msg("User tried to relearn a fact.") log.Debug().Msg("User tried to relearn a fact.")
return fmt.Errorf("Look, I already know that.") 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, Count: 0,
} }
p.LastFact = &n p.LastFact = &n
err = n.Save(p.db) err = n.Save(p.store)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error inserting fact") log.Error().Err(err).Msg("Error inserting fact")
return fmt.Errorf("My brain is overheating.") 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) { func (p *FactoidPlugin) findTrigger(fact string) (bool, *Factoid) {
fact = strings.ToLower(fact) // TODO: make sure this needs to be lowered here 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 { if err != nil {
return findAlias(p.db, fact) return findAlias(p.store, fact)
} }
return true, f return true, f
} }
@ -437,7 +300,7 @@ func (p *FactoidPlugin) sayFact(c bot.Connector, message msg.Message, fact Facto
// update fact tracking // update fact tracking
fact.Accessed = time.Now() fact.Accessed = time.Now()
fact.Count += 1 fact.Count += 1
err := fact.Save(p.db) err := fact.Save(p.store)
if err != nil { if err != nil {
log.Error(). log.Error().
Interface("fact", fact). Interface("fact", fact).
@ -506,7 +369,7 @@ func (p *FactoidPlugin) tellThemWhatThatWas(c bot.Connector, message msg.Message
msg = "Nope." msg = "Nope."
} else { } else {
msg = fmt.Sprintf("That was (#%d) '%s <%s> %s'", 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) p.Bot.Send(c, bot.Message, message.Channel, msg)
return true return true
@ -564,14 +427,14 @@ func (p *FactoidPlugin) forgetLastFact(c bot.Connector, message msg.Message) boo
return true return true
} }
err := p.LastFact.delete(p.db) err := p.LastFact.delete(p.store)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
Interface("LastFact", p.LastFact). Interface("LastFact", p.LastFact).
Msg("Error removing fact") 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.LastFact.Verb, p.LastFact.Tidbit)
p.Bot.Send(c, bot.Action, message.Channel, "hits himself over the head with a skillet") p.Bot.Send(c, bot.Action, message.Channel, "hits himself over the head with a skillet")
p.LastFact = nil p.LastFact = nil
@ -603,7 +466,7 @@ func (p *FactoidPlugin) changeFact(c bot.Connector, message msg.Message) bool {
replace := parts[2] replace := parts[2]
// replacement // replacement
result, err := getFacts(p.db, trigger, parts[1]) result, err := getFacts(p.store, trigger, parts[1])
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). 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.Tidbit = reg.ReplaceAllString(fact.Tidbit, replace)
fact.Count += 1 fact.Count += 1
fact.Accessed = time.Now() fact.Accessed = time.Now()
fact.Save(p.db) fact.Save(p.store)
} }
} else if len(parts) == 3 { } else if len(parts) == 3 {
// search for a factoid and print it // 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 { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -684,7 +547,7 @@ func (p *FactoidPlugin) register() {
to := r.Values["to"] to := r.Values["to"]
log.Debug().Msgf("alias: %+v", r) log.Debug().Msgf("alias: %+v", r)
a := aliasFromStrings(from, to) 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()) p.Bot.Send(r.Conn, bot.Message, r.Msg.Channel, err.Error())
} else { } else {
p.Bot.Send(r.Conn, bot.Action, r.Msg.Channel, "learns a new synonym") 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 // Pull a fact at random from the database
func (p *FactoidPlugin) randomFact() *Factoid { func (p *FactoidPlugin) randomFact() *Factoid {
f, err := GetSingle(p.db) f, err := GetSingle(p.store)
if err != nil { if err != nil {
fmt.Println("Error getting a fact: ", err) fmt.Println("Error getting a fact: ", err)
return nil return nil
@ -838,7 +701,7 @@ func (p *FactoidPlugin) serveAPI(w http.ResponseWriter, r *http.Request) {
return return
} }
entries, err := getFacts(p.db, info.Query, "") entries, err := getFacts(p.store, info.Query, "")
if err != nil { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
fmt.Fprint(w, err) fmt.Fprint(w, err)

View File

@ -3,13 +3,12 @@
package first package first
import ( import (
"database/sql"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -22,7 +21,7 @@ import (
type FirstPlugin struct { type FirstPlugin struct {
bot bot.Bot bot bot.Bot
config *config.Config config *config.Config
db *sqlx.DB store *bh.Store
handlers bot.HandlerTable handlers bot.HandlerTable
enabled bool enabled bool
} }
@ -38,59 +37,23 @@ type FirstEntry struct {
} }
// Insert or update the first entry // Insert or update the first entry
func (fe *FirstEntry) save(db *sqlx.DB) error { func (fe *FirstEntry) save(store *bh.Store) error {
if _, err := db.Exec(`insert into first (day, time, channel, body, nick) return store.Insert(bh.NextSequence(), fe)
values (?, ?, ?, ?, ?)`,
fe.day.Unix(),
fe.time.Unix(),
fe.channel,
fe.body,
fe.nick,
); err != nil {
return err
}
return nil
} }
func (fe *FirstEntry) delete(db *sqlx.DB) error { func (fe *FirstEntry) delete(store *bh.Store) error {
tx, err := db.Beginx() return store.Delete(fe.id, FirstEntry{})
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
} }
// NewFirstPlugin creates a new FirstPlugin with the Plugin interface // NewFirstPlugin creates a new FirstPlugin with the Plugin interface
func New(b bot.Bot) *FirstPlugin { 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", log.Info().Msgf("First plugin initialized with day: %s",
Midnight(time.Now())) Midnight(time.Now()))
fp := &FirstPlugin{ fp := &FirstPlugin{
bot: b, bot: b,
config: b.Config(), config: b.Config(),
db: b.DB(), store: b.Store(),
enabled: true, enabled: true,
} }
fp.register() fp.register()
@ -98,44 +61,14 @@ func New(b bot.Bot) *FirstPlugin {
return fp return fp
} }
func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) { func getLastFirst(store *bh.Store, channel string) (*FirstEntry, error) {
// Get last first entry fe := &FirstEntry{}
var id sql.NullInt64 err := store.FindOne(fe, bh.Where("channel").Eq(channel))
var day sql.NullInt64 if err != nil {
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")
return nil, err return nil, err
} }
log.Debug().Msgf("id: %v day %v time %v body %v nick %v", log.Debug().Msgf("id: %v day %v time %v body %v nick %v", fe)
id, day, timeEntered, body, nick) return fe, nil
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
} }
func Midnight(t time.Time) time.Time { func Midnight(t time.Time) time.Time {
@ -159,7 +92,7 @@ func (p *FirstPlugin) register() {
{Kind: bot.Message, IsCmd: false, {Kind: bot.Message, IsCmd: false,
Regex: regexp.MustCompile(`(?i)^who'?s on first the most.?$`), Regex: regexp.MustCompile(`(?i)^who'?s on first the most.?$`),
Handler: func(r bot.Request) bool { 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 { if first != nil && err == nil {
p.leaderboard(r.Conn, r.Msg.Channel) p.leaderboard(r.Conn, r.Msg.Channel)
return true return true
@ -169,7 +102,7 @@ func (p *FirstPlugin) register() {
{Kind: bot.Message, IsCmd: false, {Kind: bot.Message, IsCmd: false,
Regex: regexp.MustCompile(`(?i)^who'?s on first.?$`), Regex: regexp.MustCompile(`(?i)^who'?s on first.?$`),
Handler: func(r bot.Request) bool { 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 { if first != nil && err == nil {
p.announceFirst(r.Conn, first) p.announceFirst(r.Conn, first)
return true 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.") p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "You are not authorized to do that.")
return true return true
} }
fe, err := getLastFirst(p.db, r.Msg.Channel) fe, err := getLastFirst(p.store, r.Msg.Channel)
if err != nil { if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Could not find a first entry.") p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Could not find a first entry.")
return true return true
} }
p.enabled = false p.enabled = false
err = fe.delete(p.db) err = fe.delete(p.store)
if err != nil { if err != nil {
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, p.bot.Send(r.Conn, bot.Message, r.Msg.Channel,
fmt.Sprintf("Could not delete first entry: %s", err)) fmt.Sprintf("Could not delete first entry: %s", err))
@ -215,7 +148,7 @@ func (p *FirstPlugin) register() {
return false return false
} }
first, err := getLastFirst(p.db, r.Msg.Channel) first, err := getLastFirst(p.store, r.Msg.Channel)
if err != nil { if err != nil {
log.Error(). log.Error().
Err(err). Err(err).
@ -303,7 +236,7 @@ func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) {
nick: message.User.Name, nick: message.User.Name,
} }
log.Info().Msgf("recordFirst: %+v", first.day) log.Info().Msgf("recordFirst: %+v", first.day)
err := first.save(p.db) err := first.save(p.store)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error saving first entry") log.Error().Err(err).Msg("Error saving first entry")
return 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 { func (p *FirstPlugin) leaderboard(c bot.Connector, ch string) error {
q := `select max(channel) channel, max(nick) nick, count(id) count // todo: remove this once we verify stuff
from first //q := `select max(channel) channel, max(nick) nick, count(id) count
group by channel, nick // from first
having channel = ? // group by channel, nick
order by count desc // having channel = ?
limit 3` // order by count desc
res := []struct { // limit 3`
Channel string groups, err := p.store.FindAggregate(FirstEntry{}, bh.Where("channel").Eq(ch), "channel", "nick")
Nick string
Count int
}{}
err := p.db.Select(&res, q, ch)
if err != nil { if err != nil {
return err return err
} }
talismans := []string{":gold-trophy:", ":silver-trophy:", ":bronze-trophy:"} if len(groups) != 1 {
msg := "First leaderboard:\n" return fmt.Errorf("found %d groups but expected 1", len(groups))
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) //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 return nil
} }

View File

@ -2,12 +2,12 @@ package goals
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
"time" "time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
@ -19,7 +19,7 @@ import (
type GoalsPlugin struct { type GoalsPlugin struct {
b bot.Bot b bot.Bot
cfg *config.Config cfg *config.Config
db *sqlx.DB store *bh.Store
handlers bot.HandlerTable handlers bot.HandlerTable
} }
@ -27,30 +27,14 @@ func New(b bot.Bot) *GoalsPlugin {
p := &GoalsPlugin{ p := &GoalsPlugin{
b: b, b: b,
cfg: b.Config(), cfg: b.Config(),
db: b.DB(), store: b.Store(),
} }
p.mkDB()
p.registerCmds() p.registerCmds()
b.Register(p, bot.Help, p.help) b.Register(p, bot.Help, p.help)
counter.RegisterUpdate(p.update) counter.RegisterUpdate(p.update)
return p 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() { func (p *GoalsPlugin) registerCmds() {
p.handlers = bot.HandlerTable{ p.handlers = bot.HandlerTable{
{Kind: bot.Message, IsCmd: true, {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) { 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 { if err != nil || len(items) == 0 {
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what))
return return
@ -204,7 +188,7 @@ func (p *GoalsPlugin) checkGoal(c bot.Connector, ch, what, who string) {
Str("id", id). Str("id", id).
Str("what", what). Str("what", what).
Msg("looking for item") 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 { if err != nil {
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what))
return return
@ -253,7 +237,7 @@ func parseCmd(r *regexp.Regexp, body string) cmd {
} }
type goal struct { type goal struct {
ID int64 ID int64 `boltholdid:"ID"`
Kind string Kind string
Who string Who string
What 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) { func (p *GoalsPlugin) getGoal(who, what string) ([]*goal, error) {
gs := []*goal{} gs := []*goal{}
err := p.db.Select(&gs, `select * from goals where who = ? and what = ?`, err := p.store.Find(&gs, bh.Where("who").Eq(who).And("what").Eq(what))
who, what)
if err != nil { if err != nil {
return nil, err 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) { func (p *GoalsPlugin) getGoalKind(kind, who, what string) (*goal, error) {
g := &goal{gp: p} g := &goal{gp: p}
err := p.db.Get(g, `select * from goals where kind = ? and who = ? and what = ?`, err := p.store.FindOne(&g, bh.Where("kind").Eq(kind).And("who").Eq(who).And("what").Eq(what))
kind, who, what)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -297,15 +279,10 @@ func (p *GoalsPlugin) getGoalKind(kind, who, what string) (*goal, error) {
} }
func (g *goal) Save() error { func (g *goal) Save() error {
res, err := g.gp.db.Exec(`insert or replace into goals (who, what, kind, amount) values (?, ?, ? ,?)`, err := g.gp.store.Insert(bh.NextSequence(), &g)
g.Who, g.What, g.Kind, g.Amount)
if err != nil { if err != nil {
return err return err
} }
dbID, err := res.LastInsertId()
if err == nil && dbID != g.ID && g.ID == 0 {
g.ID = dbID
}
return nil return nil
} }
@ -313,7 +290,7 @@ func (g goal) Delete() error {
if g.ID == -1 { if g.ID == -1 {
return nil return nil
} }
_, err := g.gp.db.Exec(`delete from goals where id = ?`, g.ID) err := g.gp.store.Delete(goal{}, g.ID)
return err return err
} }

View File

@ -6,19 +6,20 @@ package inventory
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"math/rand"
"regexp" "regexp"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
) )
type InventoryPlugin struct { type InventoryPlugin struct {
*sqlx.DB *bh.Store
bot bot.Bot bot bot.Bot
config *config.Config config *config.Config
handlers bot.HandlerTable handlers bot.HandlerTable
@ -29,7 +30,7 @@ func New(b bot.Bot) *InventoryPlugin {
config := b.Config() config := b.Config()
p := &InventoryPlugin{ p := &InventoryPlugin{
DB: b.DB(), Store: b.Store(),
bot: b, bot: b,
config: config, config: config,
} }
@ -37,15 +38,15 @@ func New(b bot.Bot) *InventoryPlugin {
b.RegisterFilter("$item", p.itemFilter) b.RegisterFilter("$item", p.itemFilter)
b.RegisterFilter("$giveitem", p.giveItemFilter) b.RegisterFilter("$giveitem", p.giveItemFilter)
p.DB.MustExec(`create table if not exists inventory (
item string primary key
);`)
p.register() p.register()
return p return p
} }
type Item struct {
Name string `boltholdid:"Name"`
}
func (p *InventoryPlugin) giveItemFilter(input string) string { func (p *InventoryPlugin) giveItemFilter(input string) string {
for strings.Contains(input, "$giveitem") { for strings.Contains(input, "$giveitem") {
item := p.random() item := p.random()
@ -107,72 +108,62 @@ func (p *InventoryPlugin) register() {
} }
func (p *InventoryPlugin) removeRandom() string { func (p *InventoryPlugin) removeRandom() string {
var name string items := []Item{}
err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( err := p.Find(&items, &bh.Query{})
&name,
)
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Error finding random entry") log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR" return "IAMERROR"
} }
_, err = p.Exec(`delete from inventory where item=?`, name) item := items[rand.Intn(len(items))]
if err != nil { err = p.Delete(item.Name, Item{})
log.Error().Err(err).Msgf("Error finding random entry") return item.Name
return "IAMERROR"
}
return name
} }
func (p *InventoryPlugin) count() int { func (p *InventoryPlugin) count() int {
var output int count, err := p.Store.Count(Item{}, &bh.Query{})
err := p.QueryRow(`select count(*) as count from inventory`).Scan(&output)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error checking for item") log.Error().Err(err).Msg("Error checking for item")
return -1 return -1
} }
return output return count
} }
func (p *InventoryPlugin) random() string { func (p *InventoryPlugin) random() string {
var name string items := []Item{}
err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( err := p.Find(&items, &bh.Query{})
&name,
)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error finding random entry") log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR" return "IAMERROR"
} }
return name item := items[rand.Intn(len(items))]
return item.Name
} }
func (p *InventoryPlugin) getAll() []string { func (p *InventoryPlugin) getAll() []string {
rows, err := p.Queryx(`select item from inventory`) items := []Item{}
err := p.Find(&items, &bh.Query{})
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error getting all items") log.Error().Err(err).Msg("Error getting all items")
return []string{} return []string{}
} }
output := []string{} output := []string{}
for rows.Next() { for _, item := range items {
var item string output = append(output, item.Name)
rows.Scan(&item)
output = append(output, item)
} }
rows.Close()
return output return output
} }
func (p *InventoryPlugin) exists(i string) bool { func (p *InventoryPlugin) exists(i string) bool {
var output int count, err := p.Store.Count(Item{}, bh.Where("item").Eq(i))
err := p.QueryRow(`select count(*) as count from inventory where item=?`, i).Scan(&output)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error checking for item") log.Error().Err(err).Msg("Error checking for item")
return false return false
} }
return output > 0 return count > 0
} }
func (p *InventoryPlugin) remove(i string) { func (p *InventoryPlugin) remove(i string) {
_, err := p.Exec(`delete from inventory where item=?`, i) err := p.Store.Delete(i, Item{})
if err != nil { if err != nil {
log.Error().Msg("Error inserting new inventory item") 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 { if p.count() > max {
removed = p.removeRandom() removed = p.removeRandom()
} }
_, err := p.Exec(`INSERT INTO inventory (item) values (?)`, i) err := p.Store.Insert(i, Item{Name: i})
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error inserting new inventory item") log.Error().Err(err).Msg("Error inserting new inventory item")
} }

View File

@ -2,12 +2,12 @@ package last
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"time" "time"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
@ -16,7 +16,7 @@ import (
type LastPlugin struct { type LastPlugin struct {
b bot.Bot b bot.Bot
db *sqlx.DB store *bh.Store
c *config.Config c *config.Config
handlers bot.HandlerTable handlers bot.HandlerTable
@ -26,37 +26,14 @@ type LastPlugin struct {
func New(b bot.Bot) *LastPlugin { func New(b bot.Bot) *LastPlugin {
p := &LastPlugin{ p := &LastPlugin{
b: b, b: b,
db: b.DB(), store: b.Store(),
c: b.Config(), c: b.Config(),
channels: map[string]bool{}, channels: map[string]bool{},
} }
if err := p.migrate(); err != nil {
panic(err)
}
p.register() p.register()
return p 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() { func (p *LastPlugin) register() {
p.handlers = bot.HandlerTable{ p.handlers = bot.HandlerTable{
{ {
@ -133,13 +110,16 @@ func (p *LastPlugin) recordLast(r bot.Request) bool {
} }
} }
_, err := p.db.Exec( l := &last{
`insert into last values (?, ?, ?, ?, ?)`, Day: day.Unix(),
day.Unix(), ch, time.Now().Unix(), who, r.Msg.Body) TS: time.Now().Unix(),
if err != nil { Channel: ch,
log.Error().Err(err).Msgf("Could not record last.") 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 { type last struct {
@ -155,9 +135,9 @@ func (p *LastPlugin) yesterdaysLast(ch string) (last, error) {
midnight := first.Midnight(time.Now()) midnight := first.Midnight(time.Now())
q := `select * from last where channel = ? and day < ? and day >= ? order by day limit 1` 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()) 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 { if err != nil {
return l, err return last{}, err
} }
return l, nil return l, nil
} }

View File

@ -2,6 +2,7 @@ package newsbid
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"sort" "sort"
"strconv" "strconv"
@ -16,11 +17,12 @@ import (
type NewsBid struct { type NewsBid struct {
bot bot.Bot bot bot.Bot
db *sqlx.DB db *sqlx.DB
store *bh.Store
ws *webshit.Webshit ws *webshit.Webshit
} }
func New(b bot.Bot) *NewsBid { 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"), HNFeed: b.Config().GetString("webshit.hnfeed", "topstories"),
HNLimit: b.Config().GetInt("webshit.hnlimit", 10), HNLimit: b.Config().GetInt("webshit.hnlimit", 10),
BalanceReferesh: b.Config().GetInt("webshit.balancerefresh", 100), BalanceReferesh: b.Config().GetInt("webshit.balancerefresh", 100),
@ -28,7 +30,7 @@ func New(b bot.Bot) *NewsBid {
}) })
p := &NewsBid{ p := &NewsBid{
bot: b, bot: b,
db: b.DB(), store: b.Store(),
ws: ws, ws: ws,
} }

View File

@ -3,6 +3,7 @@ package webshit
import ( import (
"bytes" "bytes"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"net/url" "net/url"
"strconv" "strconv"
"time" "time"
@ -10,7 +11,6 @@ import (
"github.com/velour/catbase/plugins/newsbid/webshit/hn" "github.com/velour/catbase/plugins/newsbid/webshit/hn"
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/jmoiron/sqlx"
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@ -23,12 +23,12 @@ type Config struct {
} }
type Webshit struct { type Webshit struct {
db *sqlx.DB store *bh.Store
config Config config Config
} }
type Bid struct { type Bid struct {
ID int ID int64 `boltholdid:"ID"`
User string User string
Title string Title string
URL string URL string
@ -37,16 +37,12 @@ type Bid struct {
BidStr string BidStr string
PlacedScore int `db:"placed_score"` PlacedScore int `db:"placed_score"`
ProcessedScore int `db:"processed_score"` ProcessedScore int `db:"processed_score"`
Placed int64 Placed time.Time
Processed int64 Processed bool
}
func (b Bid) PlacedParsed() time.Time {
return time.Unix(b.Placed, 0)
} }
type Balance struct { type Balance struct {
User string User string `boltholdid:"User"`
Balance int Balance int
Score int Score int
} }
@ -65,34 +61,11 @@ type WeeklyResult struct {
Score int Score int
} }
func NewConfig(db *sqlx.DB, cfg Config) *Webshit { func NewConfig(store *bh.Store, cfg Config) *Webshit {
w := &Webshit{db: db, config: cfg} w := &Webshit{store: store, config: cfg}
w.setup()
return w 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) { func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) {
stories, published, err := w.GetWeekly() stories, published, err := w.GetWeekly()
if err != nil { if err != nil {
@ -105,7 +78,7 @@ func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) {
} }
var bids []Bid 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 return nil, 0, err
} }
@ -133,14 +106,20 @@ func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) {
} }
// Delete all those bids // Delete all those bids
if _, err = w.db.Exec(`update webshit_bids set processed=? where processed=0`, if err = w.store.UpdateMatching(Bid{}, bh.Where("processed").Eq(false), func(record interface{}) error {
time.Now().Unix()); err != nil { r := record.(*Bid)
r.Processed = true
return w.store.Update(r.ID, r)
}); err != nil {
return nil, 0, err return nil, 0, err
} }
// Set all balances to 100 // Set all balances to 100
if _, err = w.db.Exec(`update webshit_balances set balance=?`, if err = w.store.UpdateMatching(Balance{}, &bh.Query{}, func(record interface{}) error {
w.config.BalanceReferesh); err != nil { r := record.(*Balance)
r.Balance = w.config.BalanceReferesh
return w.store.Update(r.User, r)
}); err != nil {
return nil, 0, err return nil, 0, err
} }
@ -156,7 +135,7 @@ func (w *Webshit) checkBids(bids []Bid, storyMap map[string]hn.Item) []WeeklyRes
houseScore := 0 houseScore := 0
for _, b := range bids { for _, b := range bids {
score := w.GetScore(b.User) score := w.GetBalance(b.User).Score
if _, ok := wr[b.User]; !ok { if _, ok := wr[b.User]; !ok {
wr[b.User] = WeeklyResult{ wr[b.User] = WeeklyResult{
User: b.User, User: b.User,
@ -193,7 +172,7 @@ func (w *Webshit) checkBids(bids []Bid, storyMap map[string]hn.Item) []WeeklyRes
wr[houseName] = WeeklyResult{ wr[houseName] = WeeklyResult{
User: houseName, User: houseName,
Score: w.GetScore(houseName) + houseScore, Score: w.GetBalance(houseName).Score + houseScore,
Won: 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 // GetBalances returns the current balance for all known users
// Any unknown user has a default balance on their first bid // Any unknown user has a default balance on their first bid
func (w *Webshit) GetBalance(user string) int { func (w *Webshit) GetBalance(user string) Balance {
q := `select balance from webshit_balances where user=?` var balance Balance
var balance int err := w.store.Get(user, &balance)
err := w.db.Get(&balance, q, user)
if err != nil { if err != nil {
return 100 return Balance{
User: user,
Balance: 100,
Score: 0,
}
} }
return balance 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) { func (w *Webshit) GetAllBids() ([]Bid, error) {
var bids []Bid 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 { if err != nil {
return nil, err return nil, err
} }
@ -271,7 +243,7 @@ func (w *Webshit) GetAllBids() ([]Bid, error) {
func (w *Webshit) GetAllBalances() (Balances, error) { func (w *Webshit) GetAllBalances() (Balances, error) {
var balances Balances var balances Balances
err := w.db.Select(&balances, `select * from webshit_balances`) err := w.store.Find(&balances, &bh.Query{})
if err != nil { if err != nil {
return nil, err 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 // 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) { 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 { if amount < 0 {
return Bid{}, fmt.Errorf("cannot bid less than 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 return Bid{}, err
} }
ts := time.Now().Unix() bid := Bid{
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{
User: user, User: user,
Title: story.Title, Title: story.Title,
URL: story.URL, URL: story.URL,
Placed: ts, Placed: time.Now(),
}, err }
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 // 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 { func (w *Webshit) updateScores(results []WeeklyResult) error {
tx := w.db.MustBegin()
for _, res := range results { 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`, bal := w.GetBalance(res.User)
res.User, res.Score); err != nil { bal.Score = res.Score
if err := tx.Rollback(); err != nil { if err := w.store.Insert(res.User, bal); err != nil {
return err
}
return err return err
} }
} }
err := tx.Commit() return nil
return err
} }
func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult { func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult {

View File

@ -28,7 +28,6 @@ func New(b bot.Bot) *QuoteGame {
p := &QuoteGame{ p := &QuoteGame{
b: b, b: b,
c: b.Config(), c: b.Config(),
db: b.DB(),
currentGame: nil, currentGame: nil,
} }
p.register() p.register()

View File

@ -2,13 +2,14 @@ package remember
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"math/rand"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/fact"
@ -17,14 +18,14 @@ import (
type RememberPlugin struct { type RememberPlugin struct {
bot bot.Bot bot bot.Bot
log map[string][]msg.Message log map[string][]msg.Message
db *sqlx.DB store *bh.Store
} }
func New(b bot.Bot) *RememberPlugin { func New(b bot.Bot) *RememberPlugin {
p := &RememberPlugin{ p := &RememberPlugin{
bot: b, bot: b,
log: make(map[string][]msg.Message), log: make(map[string][]msg.Message),
db: b.DB(), store: b.Store(),
} }
b.RegisterRegex(p, bot.Message, rememberRegex, p.rememberCmd) b.RegisterRegex(p, bot.Message, rememberRegex, p.rememberCmd)
@ -78,7 +79,7 @@ func (p *RememberPlugin) rememberCmd(r bot.Request) bool {
Accessed: time.Now(), Accessed: time.Now(),
Count: 0, Count: 0,
} }
if err := fact.Save(p.db); err != nil { if err := fact.Save(p.store); err != nil {
log.Error().Err(err) log.Error().Err(err)
p.bot.Send(r.Conn, bot.Message, r.Msg.Channel, "Tell somebody I'm broke.") 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 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 // 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 // expanded to have this function execute a quote for a particular channel
func (p *RememberPlugin) randQuote() string { func RandQuote(store *bh.Store) string {
allQuotes := []fact.Factoid{}
var f fact.Factoid err := store.Find(&allQuotes, bh.Where("fact").RegExp(regexp.MustCompile(`.+ quotes$`)))
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,
)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Error getting quotes") log.Error().Err(err).Msg("Error getting quotes")
return "I had a problem getting your quote." return "I had a problem getting your quote."
} }
f.Created = time.Unix(tmpCreated, 0) f := allQuotes[rand.Intn(len(allQuotes))]
f.Accessed = time.Unix(tmpAccessed, 0)
return f.Tidbit return f.Tidbit
} }

View File

@ -3,23 +3,23 @@
package reminder package reminder
import ( import (
"errors"
"fmt"
"strconv"
"strings"
"sync"
"time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/olebedev/when" "github.com/olebedev/when"
"github.com/olebedev/when/rules/common" "github.com/olebedev/when/rules/common"
"github.com/olebedev/when/rules/en" "github.com/olebedev/when/rules/en"
bh "github.com/timshannon/bolthold"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"sync"
"time"
)
import (
"fmt"
"github.com/rs/zerolog/log"
"strconv"
"strings"
"github.com/velour/catbase/plugins/sms" "github.com/velour/catbase/plugins/sms"
) )
@ -29,7 +29,7 @@ const (
type ReminderPlugin struct { type ReminderPlugin struct {
bot bot.Bot bot bot.Bot
db *sqlx.DB store *bh.Store
mutex *sync.Mutex mutex *sync.Mutex
timer *time.Timer timer *time.Timer
config *config.Config config *config.Config
@ -46,17 +46,6 @@ type Reminder struct {
} }
func New(b bot.Bot) *ReminderPlugin { 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") dur, _ := time.ParseDuration("1h")
timer := time.NewTimer(dur) timer := time.NewTimer(dur)
timer.Stop() timer.Stop()
@ -67,7 +56,7 @@ func New(b bot.Bot) *ReminderPlugin {
plugin := &ReminderPlugin{ plugin := &ReminderPlugin{
bot: b, bot: b,
db: b.DB(), store: b.Store(),
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
timer: timer, timer: timer,
config: b.Config(), 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 { func (p *ReminderPlugin) getNextReminder() *Reminder {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return nil return nil
} }
defer rows.Close() res[0].Max("remindWhen", &reminder)
once := false return &reminder
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
} }
func (p *ReminderPlugin) addReminder(reminder *Reminder) error { func (p *ReminderPlugin) addReminder(reminder *Reminder) error {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()
_, err := p.db.Exec(`insert into reminders (fromWho, toWho, what, remindWhen, channel) values (?, ?, ?, ?, ?);`, err := p.store.Insert(bh.NextSequence(), reminder)
reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
} }
@ -273,29 +239,21 @@ func (p *ReminderPlugin) addReminder(reminder *Reminder) error {
func (p *ReminderPlugin) deleteReminder(id int64) error { func (p *ReminderPlugin) deleteReminder(id int64) error {
p.mutex.Lock() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()
res, err := p.db.Exec(`delete from reminders where id = ?;`, id) err := p.store.Delete(id, Reminder{})
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")
}
}
return err 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) 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() p.mutex.Lock()
defer p.mutex.Unlock() defer p.mutex.Unlock()
var total int q := bh.Where(filter).Eq(who)
err := p.db.Get(&total, countString) if filter == "" || who == "" {
q = &bh.Query{}
}
total, err := p.store.Count(Reminder{}, q)
if err != nil { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return "", nil return "", nil
@ -305,43 +263,36 @@ func (p *ReminderPlugin) getRemindersFormatted(filter string) (string, error) {
return "no pending reminders", nil 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 { if err != nil {
log.Error().Err(err) log.Error().Err(err)
return "", nil return "", nil
} }
defer rows.Close() txt := ""
reminders := "" for counter, reminder := range reminders {
counter := 1 txt += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, reminder.when, reminder.id)
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)
counter++ counter++
} }
remaining := total - max remaining := total - max
if remaining > 0 { 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) { func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) {
return p.getRemindersFormatted("") return p.getRemindersFormatted("", "")
} }
func (p *ReminderPlugin) getAllRemindersFromMeFormatted(channel, me string) (string, error) { 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) { 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() { func (p *ReminderPlugin) queueUpNextReminder() {

View File

@ -3,10 +3,10 @@ package rest
import ( import (
"bytes" "bytes"
"crypto/sha512" "crypto/sha512"
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url" "net/url"
@ -18,13 +18,12 @@ import (
"github.com/itchyny/gojq" "github.com/itchyny/gojq"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
) )
type RestPlugin struct { type RestPlugin struct {
b bot.Bot b bot.Bot
db *sqlx.DB store *bh.Store
handlers bot.HandlerTable handlers bot.HandlerTable
} }
@ -50,29 +49,13 @@ var postProcessors = map[string]postProcessor{
func New(b bot.Bot) *RestPlugin { func New(b bot.Bot) *RestPlugin {
p := &RestPlugin{ p := &RestPlugin{
b: b, b: b,
db: b.DB(), store: b.Store(),
handlers: bot.HandlerTable{}, handlers: bot.HandlerTable{},
} }
p.setupDB()
p.register() p.register()
return p 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() { func (p *RestPlugin) register() {
p.handlers = bot.HandlerTable{ p.handlers = bot.HandlerTable{
bot.HandlerSpec{Kind: bot.Message, IsCmd: true, bot.HandlerSpec{Kind: bot.Message, IsCmd: true,
@ -161,7 +144,7 @@ func (s *ScanableURL) Scan(src interface{}) error {
type wire struct { type wire struct {
// ID // ID
ID sql.NullInt64 ID int64 `boltholdIndex:"ID"`
// The URL to make a request to // The URL to make a request to
URL ScanableURL URL ScanableURL
// The regex which will trigger this REST action // The regex which will trigger this REST action
@ -182,40 +165,28 @@ func (w wire) String() string {
func (p *RestPlugin) getWires() ([]wire, error) { func (p *RestPlugin) getWires() ([]wire, error) {
wires := []wire{} wires := []wire{}
err := p.db.Select(&wires, `select * from wires`) err := p.store.Find(&wires, &bh.Query{})
return wires, err return wires, err
} }
func (p *RestPlugin) deleteWire(id int64) error { 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 return err
} }
func (w *wire) Update(db *sqlx.DB) error { func (w *wire) Update(store *bh.Store) error {
if !w.ID.Valid { if w.ID == -1 {
return w.Save(db) return w.Save(store)
} }
id, _ := w.ID.Value() err := store.Update(w.ID, w)
_, err := db.Exec(`update wires set url=?, parse_regex=?, return_field=? where id=?`,
w.URL.String(), w.ParseRegex.String(), w.ReturnField, id)
return err return err
} }
func (w *wire) Save(db *sqlx.DB) error { func (w *wire) Save(store *bh.Store) error {
if w.ID.Valid { if w.ID > -1 {
return w.Update(db) return w.Update(store)
} }
res, err := db.Exec(`insert into wires (url, parse_regex, return_field) values (?, ?, ?)`, return store.Insert(bh.NextSequence(), &w)
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
} }
func (p *RestPlugin) listWires(r bot.Request) bool { func (p *RestPlugin) listWires(r bot.Request) bool {
@ -227,8 +198,7 @@ func (p *RestPlugin) listWires(r bot.Request) bool {
} }
msg = "Current wires:" msg = "Current wires:"
for _, w := range wires { for _, w := range wires {
id, _ := w.ID.Value() msg += fmt.Sprintf("\n\t%d: `%s` => %s", w.ID, w.ParseRegex, w.URL)
msg += fmt.Sprintf("\n\t%d: `%s` => %s", id, w.ParseRegex, w.URL)
} }
SEND: SEND:
p.b.Send(r.Conn, bot.Message, r.Msg.Channel, msg) 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() msg = err.Error()
goto SEND goto SEND
} }
err = w.Save(p.db) err = w.Save(p.store)
if err != nil { if err != nil {
msg = err.Error() msg = err.Error()
goto SEND goto SEND

View File

@ -1,59 +1,55 @@
package roles package roles
import ( import (
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
bh "github.com/timshannon/bolthold"
) )
type Role struct { type Role struct {
ID int64 `json:"id"` ID int64 `json:"id" boltholdIndex:"ID"`
Name string `json:"name"` Name string `json:"name"`
Offering string `json:"offering"` 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{ return &Role{
Name: name, Name: name,
Offering: offering, Offering: offering,
DB: db, Store: store,
} }
} }
func (r *Role) Save() error { func (r *Role) Save() error {
q := `insert or replace into roles (name, offering) values (?, ?)` if r.ID == -1 {
res, err := r.Exec(q, r.Name, r.Offering) r.Store.Upsert(bh.NextSequence(), r)
if checkErr(err) { } else {
return err r.Store.Upsert(r.ID, r)
} }
id, err := res.LastInsertId()
if checkErr(err) {
return err
}
r.ID = id
return nil return nil
} }
func getRolesByOffering(db *sqlx.DB, offering string) ([]*Role, error) { func getRolesByOffering(store *bh.Store, offering string) ([]*Role, error) {
roles := []*Role{} roles := []*Role{}
err := db.Select(&roles, `select * from roles where offering=?`, offering) err := store.Find(&roles, bh.Where("offering").Eq(offering))
if checkErr(err) { if checkErr(err) {
return nil, err return nil, err
} }
for _, r := range roles { for _, r := range roles {
r.DB = db r.Store = store
r.Store = store
} }
return roles, nil 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{} 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) { if checkErr(err) {
return nil, err return nil, err
} }
role.DB = db role.Store = store
return role, nil return role, nil
} }

View File

@ -2,10 +2,10 @@ package roles
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"regexp" "regexp"
"strings" "strings"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -14,7 +14,7 @@ import (
type RolesPlugin struct { type RolesPlugin struct {
b bot.Bot b bot.Bot
c *config.Config c *config.Config
db *sqlx.DB store *bh.Store
h bot.HandlerTable h bot.HandlerTable
} }
@ -22,7 +22,7 @@ func New(b bot.Bot) *RolesPlugin {
p := &RolesPlugin{ p := &RolesPlugin{
b: b, b: b,
c: b.Config(), c: b.Config(),
db: b.DB(), store: b.Store(),
} }
p.RegisterWeb() p.RegisterWeb()
p.Register() p.Register()

View File

@ -4,10 +4,10 @@ import (
"embed" "embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"net/http" "net/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -19,14 +19,14 @@ var embeddedFS embed.FS
type SecretsPlugin struct { type SecretsPlugin struct {
b bot.Bot b bot.Bot
c *config.Config c *config.Config
db *sqlx.DB store *bh.Store
} }
func New(b bot.Bot) *SecretsPlugin { func New(b bot.Bot) *SecretsPlugin {
p := &SecretsPlugin{ p := &SecretsPlugin{
b: b, b: b,
c: b.Config(), c: b.Config(),
db: b.DB(), store: b.Store(),
} }
p.registerWeb() p.registerWeb()
return p return p
@ -47,8 +47,7 @@ func (p *SecretsPlugin) registerWeb() {
} }
func (p *SecretsPlugin) registerSecret(key, value string) error { func (p *SecretsPlugin) registerSecret(key, value string) error {
q := `insert into secrets (key, value) values (?, ?)` err := p.store.Insert(key, config.Secret{key, value})
_, err := p.db.Exec(q, key, value)
if err != nil { if err != nil {
return err return err
} }
@ -56,8 +55,7 @@ func (p *SecretsPlugin) registerSecret(key, value string) error {
} }
func (p *SecretsPlugin) removeSecret(key string) error { func (p *SecretsPlugin) removeSecret(key string) error {
q := `delete from secrets where key=?` err := p.store.Delete(key, config.Secret{})
_, err := p.db.Exec(q, key)
if err != nil { if err != nil {
return err return err
} }
@ -65,8 +63,7 @@ func (p *SecretsPlugin) removeSecret(key string) error {
} }
func (p *SecretsPlugin) updateSecret(key, value string) error { func (p *SecretsPlugin) updateSecret(key, value string) error {
q := `update secrets set value=? where key=?` err := p.store.Update(key, config.Secret{key, value})
_, err := p.db.Exec(q, value, key)
if err != nil { if err != nil {
return err return err
} }

View File

@ -2,6 +2,7 @@ package sms
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
@ -20,6 +21,12 @@ var plugin *SMSPlugin
type SMSPlugin struct { type SMSPlugin struct {
b bot.Bot b bot.Bot
c *config.Config c *config.Config
store *bh.Store
}
type Person struct {
Who string
Num string
} }
func New(b bot.Bot) *SMSPlugin { func New(b bot.Bot) *SMSPlugin {
@ -29,8 +36,8 @@ func New(b bot.Bot) *SMSPlugin {
plugin = &SMSPlugin{ plugin = &SMSPlugin{
b: b, b: b,
c: b.Config(), c: b.Config(),
store: b.Store(),
} }
plugin.setup()
plugin.registerWeb() plugin.registerWeb()
b.RegisterRegex(plugin, bot.Message, deleteRegex, plugin.deleteCmd) 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 { func (p *SMSPlugin) add(who, num string) error {
tx, err := p.b.DB().Beginx() person := Person{who, num}
if err != nil { return p.store.Upsert(who, person)
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
} }
func (p *SMSPlugin) delete(who string) error { func (p *SMSPlugin) delete(who string) error {
tx, err := p.b.DB().Beginx() return p.store.Delete(who, Person{})
if err != nil {
return err
}
_, err = tx.Exec(`delete from sms where who=?`, who)
if err != nil {
return err
}
err = tx.Commit()
return err
} }
func (p *SMSPlugin) get(who string) (string, error) { func (p *SMSPlugin) get(who string) (string, error) {
num := "" person := Person{}
err := p.b.DB().Get(&num, `select num from sms where who=?`, who) err := p.store.Get(who, &person)
return num, err return person.Num, err
} }
func (p *SMSPlugin) getByNumber(num string) (string, error) { func (p *SMSPlugin) getByNumber(num string) (string, error) {
who := "" person := Person{}
err := p.b.DB().Get(&who, `select who from sms where num=?`, num) err := p.store.Find(&person, bh.Where("num").Eq(num))
return who, err return person.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")
}
} }
func (p *SMSPlugin) deleteCmd(r bot.Request) bool { func (p *SMSPlugin) deleteCmd(r bot.Request) bool {

View File

@ -2,12 +2,11 @@ package tell
import ( import (
"fmt" "fmt"
bh "github.com/timshannon/bolthold"
"strings" "strings"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -16,48 +15,36 @@ type delayedMsg string
type TellPlugin struct { type TellPlugin struct {
b bot.Bot b bot.Bot
db *sqlx.DB store *bh.Store
} }
func New(b bot.Bot) *TellPlugin { func New(b bot.Bot) *TellPlugin {
tp := &TellPlugin{b, b.DB()} tp := &TellPlugin{b, b.Store()}
b.Register(tp, bot.Message, tp.message) b.Register(tp, bot.Message, tp.message)
tp.createDB()
return tp return tp
} }
type tell struct { type tell struct {
ID int ID int `boltholdIndex:"ID"`
Who string Who string
What 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 { func (t *TellPlugin) getTells() []tell {
result := []tell{} result := []tell{}
q := `select * from tell` t.store.Find(&result, &bh.Query{})
t.db.Select(&result, q)
return result return result
} }
func (t *TellPlugin) rmTell(entry tell) { func (t *TellPlugin) rmTell(entry tell) {
q := `delete from tell where id=?` if err := t.store.Delete(entry.ID, tell{}); err != nil {
if _, err := t.db.Exec(q, entry.ID); err != nil {
log.Error().Err(err).Msg("could not remove tell") log.Error().Err(err).Msg("could not remove tell")
} }
} }
func (t *TellPlugin) addTell(who, what string) error { func (t *TellPlugin) addTell(who, what string) error {
q := `insert into tell (who, what) values (?, ?)` tell := tell{Who: who, What: what}
_, err := t.db.Exec(q, who, what) err := t.store.Insert(bh.NextSequence(), tell)
if err != nil { if err != nil {
log.Error().Err(err).Msg("could not add tell") log.Error().Err(err).Msg("could not add tell")
} }