remember: fixed something? It works now.

This commit is contained in:
Chris Sexton 2019-02-15 13:22:54 -05:00
parent 6fb0990a11
commit 47a824e8da
10 changed files with 164 additions and 152 deletions

View File

@ -246,7 +246,7 @@ func (b *bot) RegisterFilter(name string, f func(string) string) {
// Register a callback // Register a callback
func (b *bot) Register(p Plugin, kind Kind, cb Callback) { func (b *bot) Register(p Plugin, kind Kind, cb Callback) {
t := reflect.TypeOf(p) t := reflect.TypeOf(p).String()
if _, ok := b.callbacks[t]; !ok { if _, ok := b.callbacks[t]; !ok {
b.callbacks[t] = make(map[Kind][]Callback) b.callbacks[t] = make(map[Kind][]Callback)
} }

View File

@ -25,6 +25,7 @@ func (b *bot) Receive(kind Kind, msg msg.Message, args ...interface{}) bool {
if kind == Message && strings.HasPrefix(msg.Body, "help") && msg.Command { if kind == Message && strings.HasPrefix(msg.Body, "help") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body)) parts := strings.Fields(strings.ToLower(msg.Body))
b.checkHelp(msg.Channel, parts) b.checkHelp(msg.Channel, parts)
log.Println("Handled a help, returning")
goto RET goto RET
} }
@ -40,7 +41,7 @@ RET:
} }
func (b *bot) runCallback(plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool { func (b *bot) runCallback(plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool {
t := reflect.TypeOf(plugin) t := reflect.TypeOf(plugin).String()
for _, cb := range b.callbacks[t][evt] { for _, cb := range b.callbacks[t][evt] {
if cb(evt, message, args...) { if cb(evt, message, args...) {
return true return true

View File

@ -3,8 +3,6 @@
package bot package bot
import ( import (
"reflect"
"github.com/jmoiron/sqlx" "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"
@ -34,7 +32,7 @@ const (
type Kind int type Kind int
type Callback func(Kind, msg.Message, ...interface{}) bool type Callback func(Kind, msg.Message, ...interface{}) bool
type CallbackMap map[reflect.Type]map[Kind][]Callback type CallbackMap map[string]map[Kind][]Callback
// Bot interface serves to allow mocking of the actual bot // Bot interface serves to allow mocking of the actual bot
type Bot interface { type Bot interface {

View File

@ -48,7 +48,7 @@ func New(c *config.Config) *SlackApp {
log.Fatalf("No slack token found. Set SLACKTOKEN env.") log.Fatalf("No slack token found. Set SLACKTOKEN env.")
} }
api := slack.New(token, slack.OptionDebug(true)) api := slack.New(token, slack.OptionDebug(false))
return &SlackApp{ return &SlackApp{
api: api, api: api,
@ -263,7 +263,7 @@ func (s *SlackApp) populateEmojiList() {
log.Println("Cannot get emoji list without slack.usertoken") log.Println("Cannot get emoji list without slack.usertoken")
return return
} }
api := slack.New(s.userToken, slack.OptionDebug(true)) api := slack.New(s.userToken, slack.OptionDebug(false))
em, err := api.GetEmoji() em, err := api.GetEmoji()
if err != nil { if err != nil {
@ -339,7 +339,7 @@ func (s *SlackApp) Who(id string) []string {
log.Println("Cannot get emoji list without slack.usertoken") log.Println("Cannot get emoji list without slack.usertoken")
return []string{s.config.Get("nick", "bot")} return []string{s.config.Get("nick", "bot")}
} }
api := slack.New(s.userToken, slack.OptionDebug(true)) api := slack.New(s.userToken, slack.OptionDebug(false))
log.Println("Who is queried for ", id) log.Println("Who is queried for ", id)
// Not super sure this is the correct call // Not super sure this is the correct call

9
go.sum
View File

@ -2,7 +2,6 @@ github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA= github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc= github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc=
@ -20,7 +19,6 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
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.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
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=
@ -37,23 +35,16 @@ github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspo
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89 h1:3D3M900hEBJJAqyKl70QuRHi5weX9+ptlQI1v+FNcQ8= github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89 h1:3D3M900hEBJJAqyKl70QuRHi5weX9+ptlQI1v+FNcQ8=
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM= github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM=
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/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8= github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec h1:vpF8Kxql6/3OvGH4y2SKtpN3WsB17mvJ8f8H1o2vucQ=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac=
github.com/yuin/gopher-lua v0.0.0-20190125051437-7b9317363aa9/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac= github.com/yuin/gopher-lua v0.0.0-20190125051437-7b9317363aa9/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac=
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-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4=
golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

View File

@ -29,6 +29,7 @@ import (
"github.com/velour/catbase/plugins/nerdepedia" "github.com/velour/catbase/plugins/nerdepedia"
"github.com/velour/catbase/plugins/picker" "github.com/velour/catbase/plugins/picker"
"github.com/velour/catbase/plugins/reaction" "github.com/velour/catbase/plugins/reaction"
"github.com/velour/catbase/plugins/remember"
"github.com/velour/catbase/plugins/reminder" "github.com/velour/catbase/plugins/reminder"
"github.com/velour/catbase/plugins/rpgORdie" "github.com/velour/catbase/plugins/rpgORdie"
"github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/rss"
@ -89,7 +90,7 @@ func main() {
b.AddPlugin(dice.New(b)) b.AddPlugin(dice.New(b))
b.AddPlugin(picker.New(b)) b.AddPlugin(picker.New(b))
b.AddPlugin(beers.New(b)) b.AddPlugin(beers.New(b))
b.AddPlugin(fact.NewRemember(b)) b.AddPlugin(remember.New(b))
b.AddPlugin(your.New(b)) b.AddPlugin(your.New(b))
b.AddPlugin(counter.New(b)) b.AddPlugin(counter.New(b))
b.AddPlugin(reminder.New(b)) b.AddPlugin(reminder.New(b))

View File

@ -23,32 +23,10 @@ func makeMessage(nick, payload string) msg.Message {
} }
} }
func makePlugin(t *testing.T) (*RememberPlugin, *Factoid, *bot.MockBot) { func makePlugin(t *testing.T) (*FactoidPlugin, *bot.MockBot) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
f := New(mb) // for DB table f := New(mb) // for DB table
p := NewRemember(mb) return f, mb
assert.NotNil(t, p)
return p, f, mb
}
// Test case
func TestCornerCaseBug(t *testing.T) {
msgs := []msg.Message{
makeMessage("user1", "I dont want to personally touch a horse dick."),
makeMessage("user3", "idk my bff rose?"),
makeMessage("user2", "!remember user1 touch"),
}
p, _, mb := makePlugin(t)
for _, m := range msgs {
p.message(bot.Message, m)
}
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "horse dick")
q, err := getSingleFact(mb.DB(), "user1 quotes")
assert.Nil(t, err)
assert.Contains(t, q.Tidbit, "horse dick")
} }
func TestReact(t *testing.T) { func TestReact(t *testing.T) {
@ -56,7 +34,7 @@ func TestReact(t *testing.T) {
makeMessage("user1", "!testing123 <react> jesus"), makeMessage("user1", "!testing123 <react> jesus"),
makeMessage("user2", "testing123"), makeMessage("user2", "testing123"),
} }
_, p, mb := makePlugin(t) p, mb := makePlugin(t)
for _, m := range msgs { for _, m := range msgs {
p.message(bot.Message, m) p.message(bot.Message, m)
@ -69,7 +47,7 @@ func TestReactCantLearnSpaces(t *testing.T) {
msgs := []msg.Message{ msgs := []msg.Message{
makeMessage("user1", "!test <react> jesus christ"), makeMessage("user1", "!test <react> jesus christ"),
} }
_, p, mb := makePlugin(t) p, mb := makePlugin(t)
for _, m := range msgs { for _, m := range msgs {
p.message(bot.Message, m) p.message(bot.Message, m)

View File

@ -22,14 +22,14 @@ import (
// respond to queries in a way that is unpredictable and fun // respond to queries in a way that is unpredictable and fun
// 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 sql.NullInt64
Fact string Fact string
Tidbit string Tidbit string
Verb string Verb string
Owner string Owner string
created time.Time Created time.Time
accessed time.Time Accessed time.Time
Count int Count int
} }
@ -38,14 +38,14 @@ type alias struct {
Next string Next string
} }
func (a *alias) resolve(db *sqlx.DB) (*factoid, error) { func (a *alias) resolve(db *sqlx.DB) (*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=?` q := `select fact, next from factoid_alias where fact=?`
var next alias var next alias
err := db.Get(&next, q, a.Next) err := db.Get(&next, q, 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(db, 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
@ -55,7 +55,7 @@ func (a *alias) resolve(db *sqlx.DB) (*factoid, error) {
return next.resolve(db) return next.resolve(db)
} }
func findAlias(db *sqlx.DB, fact string) (bool, *factoid) { func findAlias(db *sqlx.DB, fact string) (bool, *Factoid) {
q := `select * from factoid_alias where fact=?` q := `select * from factoid_alias where fact=?`
var a alias var a alias
err := db.Get(&a, q, fact) err := db.Get(&a, q, fact)
@ -89,9 +89,9 @@ 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(db *sqlx.DB) error {
var err error var err error
if f.id.Valid { if f.ID.Valid {
// update // update
_, err = db.Exec(`update factoid set _, err = db.Exec(`update factoid set
fact=?, fact=?,
@ -105,12 +105,12 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit, f.Tidbit,
f.Verb, f.Verb,
f.Owner, f.Owner,
f.accessed.Unix(), f.Accessed.Unix(),
f.Count, f.Count,
f.id.Int64) f.ID.Int64)
} else { } else {
f.created = time.Now() f.Created = time.Now()
f.accessed = time.Now() f.Accessed = time.Now()
// insert // insert
res, err := db.Exec(`insert into factoid ( res, err := db.Exec(`insert into factoid (
fact, fact,
@ -125,8 +125,8 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit, f.Tidbit,
f.Verb, f.Verb,
f.Owner, f.Owner,
f.created.Unix(), f.Created.Unix(),
f.accessed.Unix(), f.Accessed.Unix(),
f.Count, f.Count,
) )
if err != nil { if err != nil {
@ -134,23 +134,23 @@ func (f *factoid) save(db *sqlx.DB) error {
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
// hackhackhack? // hackhackhack?
f.id.Int64 = id f.ID.Int64 = id
f.id.Valid = true f.ID.Valid = true
} }
return err return err
} }
func (f *factoid) delete(db *sqlx.DB) error { func (f *Factoid) delete(db *sqlx.DB) error {
var err error var err error
if f.id.Valid { if f.ID.Valid {
_, err = db.Exec(`delete from factoid where id=?`, f.id) _, err = db.Exec(`delete from factoid where id=?`, f.ID)
} }
f.id.Valid = false f.ID.Valid = false
return err return err
} }
func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) { func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*Factoid, error) {
var fs []*factoid var fs []*Factoid
query := `select query := `select
id, id,
fact, fact,
@ -170,11 +170,11 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
return nil, err return nil, err
} }
for rows.Next() { for rows.Next() {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := rows.Scan( err := rows.Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -186,15 +186,15 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
fs = append(fs, &f) fs = append(fs, &f)
} }
return fs, err return fs, err
} }
func getSingle(db *sqlx.DB) (*factoid, error) { func GetSingle(db *sqlx.DB) (*Factoid, error) {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := db.QueryRow(`select err := db.QueryRow(`select
@ -208,7 +208,7 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
count count
from factoid from factoid
order by random() limit 1;`).Scan( order by random() limit 1;`).Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -217,13 +217,13 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
&tmpAccessed, &tmpAccessed,
&f.Count, &f.Count,
) )
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err return &f, err
} }
func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) { func GetSingleFact(db *sqlx.DB, fact string) (*Factoid, error) {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := db.QueryRow(`select err := db.QueryRow(`select
@ -239,7 +239,7 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
where fact like ? where fact like ?
order by random() limit 1;`, order by random() limit 1;`,
fact).Scan( fact).Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -248,22 +248,22 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
&tmpAccessed, &tmpAccessed,
&f.Count, &f.Count,
) )
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err return &f, err
} }
// Factoid provides the necessary plugin-wide needs // Factoid provides the necessary plugin-wide needs
type Factoid struct { type FactoidPlugin struct {
Bot bot.Bot Bot bot.Bot
NotFound []string NotFound []string
LastFact *factoid LastFact *Factoid
db *sqlx.DB db *sqlx.DB
} }
// NewFactoid creates a new Factoid with the Plugin interface // NewFactoid creates a new Factoid with the Plugin interface
func New(botInst bot.Bot) *Factoid { func New(botInst bot.Bot) *FactoidPlugin {
p := &Factoid{ p := &FactoidPlugin{
Bot: botInst, Bot: botInst,
NotFound: []string{ NotFound: []string{
"I don't know.", "I don't know.",
@ -343,7 +343,7 @@ func findAction(message string) string {
// learnFact assumes we have a learning situation and inserts a new fact // learnFact assumes we have a learning situation and inserts a new fact
// into the database // into the database
func (p *Factoid) learnFact(message msg.Message, fact, verb, tidbit string) error { func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string) error {
verb = strings.ToLower(verb) verb = strings.ToLower(verb)
if verb == "react" { if verb == "react" {
// This would be a great place to check against the API for valid emojy // This would be a great place to check against the API for valid emojy
@ -366,17 +366,17 @@ func (p *Factoid) learnFact(message msg.Message, fact, verb, tidbit string) erro
return fmt.Errorf("Look, I already know that.") return fmt.Errorf("Look, I already know that.")
} }
n := factoid{ n := Factoid{
Fact: fact, Fact: fact,
Tidbit: tidbit, Tidbit: tidbit,
Verb: verb, Verb: verb,
Owner: message.User.Name, Owner: message.User.Name,
created: time.Now(), Created: time.Now(),
accessed: time.Now(), Accessed: time.Now(),
Count: 0, Count: 0,
} }
p.LastFact = &n p.LastFact = &n
err = n.save(p.db) err = n.Save(p.db)
if err != nil { if err != nil {
log.Println("Error inserting fact: ", err) log.Println("Error inserting fact: ", err)
return fmt.Errorf("My brain is overheating.") return fmt.Errorf("My brain is overheating.")
@ -386,10 +386,10 @@ func (p *Factoid) learnFact(message msg.Message, fact, verb, tidbit string) erro
} }
// findTrigger checks to see if a given string is a trigger or not // findTrigger checks to see if a given string is a trigger or not
func (p *Factoid) 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.db, fact)
if err != nil { if err != nil {
return findAlias(p.db, fact) return findAlias(p.db, fact)
} }
@ -398,7 +398,7 @@ func (p *Factoid) findTrigger(fact string) (bool, *factoid) {
// sayFact spits out a fact to the channel and updates the fact in the database // sayFact spits out a fact to the channel and updates the fact in the database
// with new time and count information // with new time and count information
func (p *Factoid) sayFact(message msg.Message, fact factoid) { func (p *FactoidPlugin) sayFact(message msg.Message, fact Factoid) {
msg := p.Bot.Filter(message, fact.Tidbit) msg := p.Bot.Filter(message, fact.Tidbit)
full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s", full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s",
fact.Fact, fact.Verb, fact.Tidbit, fact.Fact, fact.Verb, fact.Tidbit,
@ -421,9 +421,9 @@ func (p *Factoid) sayFact(message msg.Message, fact factoid) {
} }
// 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.db)
if err != nil { if err != nil {
log.Printf("Could not update fact.\n") log.Printf("Could not update fact.\n")
log.Printf("%#v\n", fact) log.Printf("%#v\n", fact)
@ -434,7 +434,7 @@ func (p *Factoid) sayFact(message msg.Message, fact factoid) {
// trigger checks the message for its fitness to be a factoid and then hauls // trigger checks the message for its fitness to be a factoid and then hauls
// the message off to sayFact for processing if it is in fact a trigger // the message off to sayFact for processing if it is in fact a trigger
func (p *Factoid) trigger(message msg.Message) bool { func (p *FactoidPlugin) trigger(message msg.Message) bool {
minLen := p.Bot.Config().GetInt("Factoid.MinLen", 4) minLen := p.Bot.Config().GetInt("Factoid.MinLen", 4)
if len(message.Body) > minLen || message.Command || message.Body == "..." { if len(message.Body) > minLen || message.Command || message.Body == "..." {
if ok, fact := p.findTrigger(message.Body); ok { if ok, fact := p.findTrigger(message.Body); ok {
@ -453,20 +453,20 @@ func (p *Factoid) trigger(message msg.Message) bool {
} }
// tellThemWhatThatWas is a hilarious name for a function. // tellThemWhatThatWas is a hilarious name for a function.
func (p *Factoid) tellThemWhatThatWas(message msg.Message) bool { func (p *FactoidPlugin) tellThemWhatThatWas(message msg.Message) bool {
fact := p.LastFact fact := p.LastFact
var msg string var msg string
if fact == nil { if fact == nil {
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.Int64, fact.Fact, fact.Verb, fact.Tidbit)
} }
p.Bot.Send(bot.Message, message.Channel, msg) p.Bot.Send(bot.Message, message.Channel, msg)
return true return true
} }
func (p *Factoid) learnAction(message msg.Message, action string) bool { func (p *FactoidPlugin) learnAction(message msg.Message, action string) bool {
body := message.Body body := message.Body
parts := strings.SplitN(body, action, 2) parts := strings.SplitN(body, action, 2)
@ -512,7 +512,7 @@ func changeOperator(body string) string {
// If the user requesting forget is either the owner of the last learned fact or // If the user requesting forget is either the owner of the last learned fact or
// an admin, it may be deleted // an admin, it may be deleted
func (p *Factoid) forgetLastFact(message msg.Message) bool { func (p *FactoidPlugin) forgetLastFact(message msg.Message) bool {
if p.LastFact == nil { if p.LastFact == nil {
p.Bot.Send(bot.Message, message.Channel, "I refuse.") p.Bot.Send(bot.Message, message.Channel, "I refuse.")
return true return true
@ -522,7 +522,7 @@ func (p *Factoid) forgetLastFact(message msg.Message) bool {
if err != nil { if err != nil {
log.Println("Error removing fact: ", p.LastFact, err) log.Println("Error removing fact: ", p.LastFact, err)
} }
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.Int64, p.LastFact.Fact,
p.LastFact.Verb, p.LastFact.Tidbit) p.LastFact.Verb, p.LastFact.Tidbit)
p.Bot.Send(bot.Action, message.Channel, "hits himself over the head with a skillet") p.Bot.Send(bot.Action, message.Channel, "hits himself over the head with a skillet")
p.LastFact = nil p.LastFact = nil
@ -531,7 +531,7 @@ func (p *Factoid) forgetLastFact(message msg.Message) bool {
} }
// Allow users to change facts with a simple regexp // Allow users to change facts with a simple regexp
func (p *Factoid) changeFact(message msg.Message) bool { func (p *FactoidPlugin) changeFact(message msg.Message) bool {
oper := changeOperator(message.Body) oper := changeOperator(message.Body)
parts := strings.SplitN(message.Body, oper, 2) parts := strings.SplitN(message.Body, oper, 2)
userexp := strings.TrimSpace(parts[1]) userexp := strings.TrimSpace(parts[1])
@ -571,8 +571,8 @@ func (p *Factoid) changeFact(message msg.Message) bool {
fact.Verb = reg.ReplaceAllString(fact.Verb, replace) fact.Verb = reg.ReplaceAllString(fact.Verb, replace)
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.db)
} }
} else if len(parts) == 3 { } else if len(parts) == 3 {
// search for a factoid and print it // search for a factoid and print it
@ -614,7 +614,7 @@ func (p *Factoid) changeFact(message msg.Message) bool {
// Message responds to the bot hook on recieving messages. // Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message. // This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins. // Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *Factoid) message(kind bot.Kind, message msg.Message, args ...interface{}) bool { func (p *FactoidPlugin) message(kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "what was that?" { if strings.ToLower(message.Body) == "what was that?" {
return p.tellThemWhatThatWas(message) return p.tellThemWhatThatWas(message)
} }
@ -674,15 +674,15 @@ func (p *Factoid) message(kind bot.Kind, message msg.Message, args ...interface{
} }
// Help responds to help requests. Every plugin must implement a help function. // Help responds to help requests. Every plugin must implement a help function.
func (p *Factoid) help(kind bot.Kind, message msg.Message, args ...interface{}) bool { func (p *FactoidPlugin) help(kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.Send(bot.Message, message.Channel, "I can learn facts and spit them back out. You can say \"this is that\" or \"he <has> $5\". Later, trigger the factoid by just saying the trigger word, \"this\" or \"he\" in these examples.") p.Bot.Send(bot.Message, message.Channel, "I can learn facts and spit them back out. You can say \"this is that\" or \"he <has> $5\". Later, trigger the factoid by just saying the trigger word, \"this\" or \"he\" in these examples.")
p.Bot.Send(bot.Message, message.Channel, "I can also figure out some variables including: $nonzero, $digit, $nick, and $someone.") p.Bot.Send(bot.Message, message.Channel, "I can also figure out some variables including: $nonzero, $digit, $nick, and $someone.")
return true return true
} }
// Pull a fact at random from the database // Pull a fact at random from the database
func (p *Factoid) randomFact() *factoid { func (p *FactoidPlugin) randomFact() *Factoid {
f, err := getSingle(p.db) f, err := GetSingle(p.db)
if err != nil { if err != nil {
fmt.Println("Error getting a fact: ", err) fmt.Println("Error getting a fact: ", err)
return nil return nil
@ -691,7 +691,7 @@ func (p *Factoid) randomFact() *factoid {
} }
// factTimer spits out a fact at a given interval and with given probability // factTimer spits out a fact at a given interval and with given probability
func (p *Factoid) factTimer(channel string) { func (p *FactoidPlugin) factTimer(channel string) {
quoteTime := p.Bot.Config().GetInt("Factoid.QuoteTime", 30) quoteTime := p.Bot.Config().GetInt("Factoid.QuoteTime", 30)
if quoteTime == 0 { if quoteTime == 0 {
quoteTime = 30 quoteTime = 30
@ -739,7 +739,7 @@ func (p *Factoid) factTimer(channel string) {
} }
// Register any web URLs desired // Register any web URLs desired
func (p *Factoid) registerWeb() { func (p *FactoidPlugin) registerWeb() {
http.HandleFunc("/factoid/req", p.serveQuery) http.HandleFunc("/factoid/req", p.serveQuery)
http.HandleFunc("/factoid", p.serveQuery) http.HandleFunc("/factoid", p.serveQuery)
p.Bot.RegisterWeb("/factoid", "Factoid") p.Bot.RegisterWeb("/factoid", "Factoid")
@ -755,7 +755,7 @@ func linkify(text string) template.HTML {
return template.HTML(strings.Join(parts, " ")) return template.HTML(strings.Join(parts, " "))
} }
func (p *Factoid) serveQuery(w http.ResponseWriter, r *http.Request) { func (p *FactoidPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
context := make(map[string]interface{}) context := make(map[string]interface{})
funcMap := template.FuncMap{ funcMap := template.FuncMap{
// The name "title" is what the function will be called in the template text. // The name "title" is what the function will be called in the template text.

View File

@ -1,6 +1,4 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. package remember
package fact
import ( import (
"fmt" "fmt"
@ -11,38 +9,32 @@ import (
"github.com/jmoiron/sqlx" "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"
) )
// This is a skeleton plugin to serve as an example and quick copy/paste for new
// plugins.
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 db *sqlx.DB
} }
// NewRememberPlugin creates a new RememberPlugin with the Plugin interface func New(b bot.Bot) *RememberPlugin {
func NewRemember(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(), db: b.DB(),
} }
b.Register(p, bot.Message, p.message) b.Register(p, bot.Message, p.message)
b.Register(p, bot.Message, p.help) b.Register(p, bot.Help, p.help)
return &p
return p
} }
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the
// users message. Otherwise, the function returns false and the bot continues
// execution of other plugins.
func (p *RememberPlugin) message(kind bot.Kind, message msg.Message, args ...interface{}) bool { func (p *RememberPlugin) message(kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "quote" && message.Command { if strings.ToLower(message.Body) == "quote" && message.Command {
q := p.randQuote() q := p.randQuote()
p.Bot.Send(bot.Message, message.Channel, q) p.bot.Send(bot.Message, message.Channel, q)
// is it evil not to remember that the user said quote? // is it evil not to remember that the user said quote?
return true return true
@ -50,16 +42,16 @@ func (p *RememberPlugin) message(kind bot.Kind, message msg.Message, args ...int
user := message.User user := message.User
parts := strings.Fields(message.Body) parts := strings.Fields(message.Body)
if message.Command && len(parts) >= 3 && if message.Command && len(parts) >= 3 &&
strings.ToLower(parts[0]) == "remember" { strings.ToLower(parts[0]) == "remember" {
// we have a remember! // we have a remember!
// look through the logs and find parts[1] as a user, if not, // look through the logs and find parts[1] as a user, if not,
// fuck this hoser // fuck this hoser
nick := parts[1] nick := parts[1]
snip := strings.Join(parts[2:], " ") snip := strings.Join(parts[2:], " ")
for i := len(p.Log[message.Channel]) - 1; i >= 0; i-- { for i := len(p.log[message.Channel]) - 1; i >= 0; i-- {
entry := p.Log[message.Channel][i] entry := p.log[message.Channel][i]
log.Printf("Comparing %s:%s with %s:%s", log.Printf("Comparing %s:%s with %s:%s",
entry.User.Name, entry.Body, nick, snip) entry.User.Name, entry.Body, nick, snip)
if strings.ToLower(entry.User.Name) == strings.ToLower(nick) && if strings.ToLower(entry.User.Name) == strings.ToLower(nick) &&
@ -78,18 +70,18 @@ func (p *RememberPlugin) message(kind bot.Kind, message msg.Message, args ...int
trigger := fmt.Sprintf("%s quotes", entry.User.Name) trigger := fmt.Sprintf("%s quotes", entry.User.Name)
fact := factoid{ fact := fact.Factoid{
Fact: strings.ToLower(trigger), Fact: strings.ToLower(trigger),
Verb: "reply", Verb: "reply",
Tidbit: msg, Tidbit: msg,
Owner: user.Name, Owner: user.Name,
created: time.Now(), Created: time.Now(),
accessed: time.Now(), Accessed: time.Now(),
Count: 0, Count: 0,
} }
if err := fact.save(p.db); err != nil { if err := fact.Save(p.db); err != nil {
log.Println("ERROR!!!!:", err) log.Println("ERROR!!!!:", err)
p.Bot.Send(bot.Message, message.Channel, "Tell somebody I'm broke.") p.bot.Send(bot.Message, message.Channel, "Tell somebody I'm broke.")
} }
log.Println("Remembering factoid:", msg) log.Println("Remembering factoid:", msg)
@ -97,30 +89,28 @@ func (p *RememberPlugin) message(kind bot.Kind, message msg.Message, args ...int
// sorry, not creative with names so we're reusing msg // sorry, not creative with names so we're reusing msg
msg = fmt.Sprintf("Okay, %s, remembering '%s'.", msg = fmt.Sprintf("Okay, %s, remembering '%s'.",
message.User.Name, msg) message.User.Name, msg)
p.Bot.Send(bot.Message, message.Channel, msg) p.bot.Send(bot.Message, message.Channel, msg)
p.recordMsg(message) p.recordMsg(message)
return true return true
} }
} }
p.bot.Send(bot.Message, message.Channel, "Sorry, I don't know that phrase.")
p.Bot.Send(bot.Message, message.Channel, "Sorry, I don't know that phrase.")
p.recordMsg(message) p.recordMsg(message)
return true return true
} }
p.recordMsg(message) p.recordMsg(message)
return false return false
} }
// Help responds to help requests. Every plugin must implement a help function.
func (p *RememberPlugin) help(kind bot.Kind, message msg.Message, args ...interface{}) bool { func (p *RememberPlugin) help(kind bot.Kind, message msg.Message, args ...interface{}) bool {
msg := "remember will let you quote your idiot friends. Just type " +
msg := "!remember will let you quote your idiot friends. Just type " +
"!remember <nick> <snippet> to remember what they said. Snippet can " + "!remember <nick> <snippet> to remember what they said. Snippet can " +
"be any part of their message. Later on, you can ask for a random " + "be any part of their message. Later on, you can ask for a random " +
"!quote." "!quote."
p.Bot.Send(bot.Message, message.Channel, msg) p.bot.Send(bot.Message, message.Channel, msg)
return true return true
} }
@ -129,12 +119,12 @@ func (p *RememberPlugin) help(kind bot.Kind, message msg.Message, args ...interf
// 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 (p *RememberPlugin) randQuote() string {
var f factoid var f fact.Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := p.db.QueryRow(`select * from factoid where fact like '%quotes' err := p.db.QueryRow(`select * from factoid where fact like '%quotes'
order by random() limit 1;`).Scan( order by random() limit 1;`).Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -147,13 +137,13 @@ func (p *RememberPlugin) randQuote() string {
log.Println("Error getting quotes: ", err) log.Println("Error getting quotes: ", err)
return "I had a problem getting your quote." return "I had a problem getting your quote."
} }
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
return f.Tidbit return f.Tidbit
} }
func (p *RememberPlugin) recordMsg(message msg.Message) { func (p *RememberPlugin) recordMsg(message msg.Message) {
log.Printf("Logging message: %s: %s", message.User.Name, message.Body) log.Printf("Logging message: %s: %s", message.User.Name, message.Body)
p.Log[message.Channel] = append(p.Log[message.Channel], message) p.log[message.Channel] = append(p.log[message.Channel], message)
} }

View File

@ -0,0 +1,53 @@
package remember
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
"github.com/velour/catbase/plugins/fact"
)
func makeMessage(nick, payload string) msg.Message {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
User: &user.User{Name: nick},
Channel: "test",
Body: payload,
Command: isCmd,
}
}
func makePlugin(t *testing.T) (*RememberPlugin, *fact.FactoidPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
f := fact.New(mb) // for DB table
p := New(mb)
assert.NotNil(t, p)
return p, f, mb
}
// Test case
func TestCornerCaseBug(t *testing.T) {
msgs := []msg.Message{
makeMessage("user1", "I dont want to personally touch a horse dick."),
makeMessage("user3", "idk my bff rose?"),
makeMessage("user2", "!remember user1 touch"),
}
p, _, mb := makePlugin(t)
for _, m := range msgs {
p.message(bot.Message, m)
}
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "horse dick")
q, err := fact.GetSingleFact(mb.DB(), "user1 quotes")
assert.Nil(t, err)
assert.Contains(t, q.Tidbit, "horse dick")
}