Migrate factoids to SQL

This commit is contained in:
Chris Sexton 2016-01-17 10:29:30 -05:00
parent 88c2736f5a
commit c91f4a8535
3 changed files with 279 additions and 147 deletions

View File

@ -67,7 +67,7 @@ func main() {
Bot.AddHandler("skeleton", plugins.NewSkeletonPlugin(Bot)) Bot.AddHandler("skeleton", plugins.NewSkeletonPlugin(Bot))
Bot.AddHandler("your", plugins.NewYourPlugin(Bot)) Bot.AddHandler("your", plugins.NewYourPlugin(Bot))
// catches anything left, will always return true // catches anything left, will always return true
// Bot.AddHandler("factoid", plugins.NewFactoidPlugin(Bot)) Bot.AddHandler("factoid", plugins.NewFactoidPlugin(Bot))
handleConnection() handleConnection()

View File

@ -3,6 +3,7 @@
package plugins package plugins
import ( import (
"database/sql"
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
@ -13,41 +14,193 @@ import (
"time" "time"
"github.com/chrissexton/alepale/bot" "github.com/chrissexton/alepale/bot"
"labix.org/v2/mgo"
"labix.org/v2/mgo/bson"
) )
// The factoid plugin provides a learning system to the bot so that it can // The factoid plugin provides a learning system to the bot so that it can
// 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 bson.ObjectId `bson:"_id,omitempty"` id sql.NullInt64
Idx int fact string
Trigger string tidbit string
Operator string verb string
FullText string owner string
Action string created time.Time
CreatedBy string accessed time.Time
DateCreated time.Time count int
LastAccessed time.Time }
Updated time.Time
AccessCount int func (f *factoid) save(db *sql.DB) error {
var err error
if f.id.Valid {
// update
_, err = db.Exec(`update factoid set
fact=?,
tidbit=?,
verb=?,
owner=?,
accessed=?,
count=?
where id=?`,
f.fact,
f.tidbit,
f.verb,
f.owner,
f.accessed.Unix(),
f.count,
f.id.Int64)
} else {
f.created = time.Now()
f.accessed = time.Now()
// insert
res, err := db.Exec(`insert into factoid (
fact,
tidbit,
verb,
owner,
created,
accessed,
count
) values (?, ?, ?, ?, ?, ?, ?);`,
f.fact,
f.tidbit,
f.verb,
f.owner,
f.created.Unix(),
f.accessed.Unix(),
f.count,
)
if err != nil {
return err
}
id, err := res.LastInsertId()
// hackhackhack?
f.id.Int64 = id
f.id.Valid = true
}
return err
}
func (f *factoid) delete(db *sql.DB) error {
var err error
if f.id.Valid {
_, err = db.Exec(`delete from factoid where id=?`, f.id)
}
f.id.Valid = false
return err
}
func getFacts(db *sql.DB, fact string) ([]*factoid, error) {
var fs []*factoid
rows, err := db.Query(`select
id,
fact,
tidbit,
verb,
owner,
created,
accessed,
count
from factoid
where fact like ?;`,
fact)
for rows.Next() {
var f factoid
var tmpCreated int64
var tmpAccessed int64
err := rows.Scan(
&f.id,
&f.fact,
&f.tidbit,
&f.verb,
&f.owner,
&tmpCreated,
&tmpAccessed,
&f.count,
)
if err != nil {
return nil, err
}
f.created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0)
fs = append(fs, &f)
}
return fs, err
}
func getSingle(db *sql.DB) (*factoid, error) {
var f factoid
var tmpCreated int64
var tmpAccessed int64
err := db.QueryRow(`select
id,
fact,
tidbit,
verb,
owner,
created,
accessed,
count
from factoid
order by random() limit 1;`).Scan(
&f.id,
&f.fact,
&f.tidbit,
&f.verb,
&f.owner,
&tmpCreated,
&tmpAccessed,
&f.count,
)
f.created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0)
return &f, err
}
func getSingleFact(db *sql.DB, fact string) (*factoid, error) {
var f factoid
var tmpCreated int64
var tmpAccessed int64
err := db.QueryRow(`select
id,
fact,
tidbit,
verb,
owner,
created,
accessed,
count
from factoid
where fact like ?
order by random() limit 1;`,
fact).Scan(
&f.id,
&f.fact,
&f.tidbit,
&f.verb,
&f.owner,
&tmpCreated,
&tmpAccessed,
&f.count,
)
f.created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0)
return &f, err
} }
// FactoidPlugin provides the necessary plugin-wide needs // FactoidPlugin provides the necessary plugin-wide needs
type FactoidPlugin struct { type FactoidPlugin struct {
Bot *bot.Bot Bot *bot.Bot
Coll *mgo.Collection
NotFound []string NotFound []string
LastFact *Factoid LastFact *factoid
db *sql.DB
} }
// NewFactoidPlugin creates a new FactoidPlugin with the Plugin interface // NewFactoidPlugin creates a new FactoidPlugin with the Plugin interface
func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin { func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin {
p := &FactoidPlugin{ p := &FactoidPlugin{
Bot: botInst, Bot: botInst,
Coll: nil,
NotFound: []string{ NotFound: []string{
"I don't know.", "I don't know.",
"NONONONO", "NONONONO",
@ -56,8 +209,23 @@ func NewFactoidPlugin(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,
} }
p.LoadData()
_, 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
);`)
if err != nil {
log.Fatal(err)
}
for _, channel := range botInst.Config.Channels { for _, channel := range botInst.Config.Channels {
go p.factTimer(channel) go p.factTimer(channel)
@ -67,7 +235,7 @@ func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin {
if ok, fact := p.findTrigger(p.Bot.Config.StartupFact); ok { if ok, fact := p.findTrigger(p.Bot.Config.StartupFact); ok {
p.sayFact(bot.Message{ p.sayFact(bot.Message{
Channel: channel, Channel: channel,
Body: "speed test", Body: "speed test", // BUG: This is defined in the config too
Command: true, Command: true,
Action: false, Action: false,
}, *fact) }, *fact)
@ -99,93 +267,82 @@ 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 *FactoidPlugin) learnFact(message bot.Message, trigger, operator, fact string) bool { func (p *FactoidPlugin) learnFact(message bot.Message, fact, verb, tidbit string) bool {
// if it's an action, we only want the fact part of it in the fulltext verb = strings.ToLower(verb)
full := fact
if operator != "action" && operator != "reply" {
full = fmt.Sprintf("%s %s %s", trigger, operator, fact)
}
trigger = strings.ToLower(trigger)
q := p.Coll.Find(bson.M{"trigger": trigger, "operator": operator, "fulltext": full}) var count sql.NullInt64
if n, _ := q.Count(); n != 0 { err := p.db.QueryRow(`select count(*) from factoid
where fact=? and verb=? and tidbit=?`,
fact, verb, tidbit).Scan(&count)
if err != nil {
log.Println("Error counting facts: ", err)
return false
} else if count.Valid && count.Int64 != 0 {
log.Println("User tried to relearn a fact.")
return false return false
} }
// definite error here if no func setup n := factoid{
// let's just aggregate fact: fact,
var count map[string]interface{} tidbit: tidbit,
query := []bson.M{{ verb: verb,
"$group": bson.M{ owner: message.User.Name,
"_id": nil, created: time.Now(),
"idx": bson.M{ accessed: time.Now(),
"$max": "$idx", count: 0,
}, }
}, p.LastFact = &n
}} err = n.save(p.db)
pipe := p.Coll.Pipe(query)
err := pipe.One(&count)
if err != nil { if err != nil {
panic(err) log.Println("Error inserting fact: ", err)
return false
} }
id := count["idx"].(int) + 1
newfact := Factoid{
Id: bson.NewObjectId(),
Idx: id,
Trigger: trigger,
Operator: operator,
FullText: full,
Action: fact,
CreatedBy: message.User.Name,
}
p.Coll.Insert(newfact)
p.LastFact = &newfact
return true return true
} }
// 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 *FactoidPlugin) findTrigger(message string) (bool, *Factoid) { func (p *FactoidPlugin) findTrigger(fact string) (bool, *factoid) {
var results []Factoid fact = strings.ToLower(fact) // TODO: make sure this needs to be lowered here
iter := p.Coll.Find(bson.M{"trigger": strings.ToLower(message)}).Iter()
err := iter.All(&results) f, err := getSingleFact(p.db, fact)
if err != nil { if err != nil {
log.Printf("Looking for trigger '%s', got err: %s", fact, err)
return false, nil return false, nil
} }
return true, f
nfacts := len(results)
if nfacts == 0 {
return false, nil
}
fact := results[rand.Intn(nfacts)]
return true, &fact
} }
// 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 *FactoidPlugin) sayFact(message bot.Message, fact Factoid) { func (p *FactoidPlugin) sayFact(message bot.Message, fact factoid) {
msg := p.Bot.Filter(message, fact.FullText) msg := p.Bot.Filter(message, fact.tidbit)
full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s",
fact.fact, fact.verb, fact.tidbit,
))
for i, m := 0, strings.Split(msg, "$and"); i < len(m) && i < 4; i++ { for i, m := 0, strings.Split(msg, "$and"); i < len(m) && i < 4; i++ {
msg := strings.TrimSpace(m[i]) msg := strings.TrimSpace(m[i])
if len(msg) == 0 { if len(msg) == 0 {
continue continue
} }
if fact.Operator == "action" { if fact.verb == "action" {
p.Bot.SendAction(message.Channel, msg) p.Bot.SendAction(message.Channel, msg)
} else { } else if fact.verb == "reply" {
p.Bot.SendMessage(message.Channel, msg) p.Bot.SendMessage(message.Channel, msg)
} else {
p.Bot.SendMessage(message.Channel, full)
} }
} }
// update fact tracking // update fact tracking
fact.LastAccessed = time.Now() fact.accessed = time.Now()
fact.AccessCount += 1 fact.count += 1
err := p.Coll.UpdateId(fact.Id, fact) err := fact.save(p.db)
if err != nil { if err != nil {
fmt.Printf("Could not update fact.\n") log.Printf("Could not update fact.\n")
fmt.Printf("%#v\n", fact) log.Printf("%#v\n", fact)
log.Println(err)
} }
p.LastFact = &fact p.LastFact = &fact
} }
@ -217,7 +374,7 @@ func (p *FactoidPlugin) tellThemWhatThatWas(message bot.Message) bool {
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.Idx, fact.Trigger, fact.Operator, fact.Action) fact.id, fact.fact, fact.verb, fact.tidbit)
} }
p.Bot.SendMessage(message.Channel, msg) p.Bot.SendMessage(message.Channel, msg)
return true return true
@ -274,13 +431,13 @@ func (p *FactoidPlugin) forgetLastFact(message bot.Message) bool {
p.Bot.SendMessage(message.Channel, "I refuse.") p.Bot.SendMessage(message.Channel, "I refuse.")
return true return true
} }
if message.User.Admin || message.User.Name == p.LastFact.CreatedBy { if message.User.Admin || message.User.Name == p.LastFact.owner {
err := p.Coll.Remove(bson.M{"_id": p.LastFact.Id}) err := p.LastFact.delete(p.db)
if err != nil { if err != nil {
panic(err) log.Println("Error removing fact: ", p.LastFact, err)
} }
fmt.Printf("Forgot #%d: %s %s %s\n", p.LastFact.Idx, p.LastFact.Trigger, fmt.Printf("Forgot #%d: %s %s %s\n", p.LastFact.id, p.LastFact.fact,
p.LastFact.Operator, p.LastFact.Action) p.LastFact.verb, p.LastFact.tidbit)
p.Bot.SendAction(message.Channel, "hits himself over the head with a skillet") p.Bot.SendAction(message.Channel, "hits himself over the head with a skillet")
p.LastFact = nil p.LastFact = nil
} else { } else {
@ -307,14 +464,13 @@ func (p *FactoidPlugin) changeFact(message bot.Message) bool {
replace := parts[2] replace := parts[2]
// replacement // replacement
var result []Factoid result, err := getFacts(p.db, trigger)
iter := p.Coll.Find(bson.M{"trigger": trigger}) if err != nil {
if message.User.Admin && userexp[len(userexp)-1] == 'g' { log.Println("Error getting facts: ", trigger, err)
iter.All(&result) }
} else { if !(message.User.Admin && userexp[len(userexp)-1] == 'g') {
result = make([]Factoid, 1) result = result[:1]
iter.One(&result[0]) if result[0].owner != message.User.Name && !message.User.Admin {
if result[0].CreatedBy != message.User.Name && !message.User.Admin {
p.Bot.SendMessage(message.Channel, "That's not your fact to edit.") p.Bot.SendMessage(message.Channel, "That's not your fact to edit.")
return true return true
} }
@ -328,29 +484,26 @@ func (p *FactoidPlugin) changeFact(message bot.Message) bool {
return false return false
} }
for _, fact := range result { for _, fact := range result {
fact.FullText = reg.ReplaceAllString(fact.FullText, replace) fact.fact = reg.ReplaceAllString(fact.fact, replace)
fact.Trigger = reg.ReplaceAllString(fact.Trigger, replace) fact.fact = strings.ToLower(fact.fact)
fact.Trigger = strings.ToLower(fact.Trigger) fact.verb = reg.ReplaceAllString(fact.verb, replace)
fact.Operator = reg.ReplaceAllString(fact.Operator, replace) fact.tidbit = reg.ReplaceAllString(fact.tidbit, replace)
fact.Action = reg.ReplaceAllString(fact.Action, replace) fact.count += 1
fact.AccessCount += 1 fact.accessed = time.Now()
fact.LastAccessed = time.Now() fact.save(p.db)
fact.Updated = time.Now()
p.Coll.UpdateId(fact.Id, fact)
} }
} else if len(parts) == 3 { } else if len(parts) == 3 {
// search for a factoid and print it // search for a factoid and print it
var result []Factoid result, err := getFacts(p.db, trigger)
iter := p.Coll.Find(bson.M{"trigger": trigger, if err != nil {
"fulltext": bson.M{"$regex": parts[1]}}) log.Println("Error getting facts: ", trigger, err)
count, _ := iter.Count() }
count := len(result)
if parts[2] == "g" { if parts[2] == "g" {
// summarize // summarize
iter.Limit(4).All(&result) result = result[:4]
} else { } else {
result = make([]Factoid, 1) p.sayFact(message, *result[0])
iter.One(&result[0])
p.sayFact(message, result[0])
return true return true
} }
msg := fmt.Sprintf("%s ", trigger) msg := fmt.Sprintf("%s ", trigger)
@ -358,7 +511,7 @@ func (p *FactoidPlugin) changeFact(message bot.Message) bool {
if i != 0 { if i != 0 {
msg = fmt.Sprintf("%s |", msg) msg = fmt.Sprintf("%s |", msg)
} }
msg = fmt.Sprintf("%s <%s> %s", msg, fact.Operator, fact.Action) msg = fmt.Sprintf("%s <%s> %s", msg, fact.verb, fact.tidbit)
} }
if count > 4 { if count > 4 {
msg = fmt.Sprintf("%s | ...and %d others", msg, count) msg = fmt.Sprintf("%s | ...and %d others", msg, count)
@ -416,16 +569,6 @@ func (p *FactoidPlugin) Message(message bot.Message) bool {
return true return true
} }
// LoadData imports any configuration data into the plugin. This is not strictly necessary other
// than the fact that the Plugin interface demands it exist. This may be deprecated at a later
// date.
func (p *FactoidPlugin) LoadData() {
// Mongo is removed, this plugin will crash if started
log.Fatal("The Factoid plugin has not been upgraded to SQL yet.")
// p.Coll = p.Bot.Db.C("factoid")
rand.Seed(time.Now().Unix())
}
// 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 *FactoidPlugin) Help(channel string, parts []string) { func (p *FactoidPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(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.SendMessage(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.")
@ -438,20 +581,13 @@ func (p *FactoidPlugin) Event(kind string, message bot.Message) bool {
} }
// 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 {
var fact Factoid f, err := getSingle(p.db)
nFacts, err := p.Coll.Count()
if err != nil { if err != nil {
fmt.Println("Error getting a fact: ", err)
return nil return nil
} }
return f
// Possible bug here with no db
if err := p.Coll.Find(nil).Skip(rand.Intn(nFacts)).One(&fact); err != nil {
log.Println("Couldn't get next...")
}
return &fact
} }
// 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
@ -518,8 +654,9 @@ func (p *FactoidPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
"linkify": linkify, "linkify": linkify,
} }
if e := r.FormValue("entry"); e != "" { if e := r.FormValue("entry"); e != "" {
var entries []Factoid var entries []factoid
p.Coll.Find(bson.M{"trigger": bson.M{"$regex": strings.ToLower(e)}}).All(&entries) // TODO: Fix the web interface with text search?
// p.Coll.Find(bson.M{"trigger": bson.M{"$regex": strings.ToLower(e)}}).All(&entries)
context["Count"] = fmt.Sprintf("%d", len(entries)) context["Count"] = fmt.Sprintf("%d", len(entries))
context["Entries"] = entries context["Entries"] = entries
context["Search"] = e context["Search"] = e

View File

@ -94,7 +94,6 @@ func (p *RememberPlugin) Message(message bot.Message) bool {
if len(msgs) == len(snips) { if len(msgs) == len(snips) {
msg := strings.Join(msgs, "$and") msg := strings.Join(msgs, "$and")
var funcres bson.M
// Needs to be upgraded to SQL // Needs to be upgraded to SQL
// err := p.Bot.Db.Run( // err := p.Bot.Db.Run(
// bson.M{"eval": "return counter(\"factoid\");"}, // bson.M{"eval": "return counter(\"factoid\");"},
@ -104,19 +103,15 @@ func (p *RememberPlugin) Message(message bot.Message) bool {
// if err != nil { // if err != nil {
// panic(err) // panic(err)
// } // }
id := int(funcres["retval"].(float64))
fact := Factoid{ fact := factoid{
Id: bson.NewObjectId(), fact: strings.ToLower(trigger),
Idx: id, verb: "reply",
Trigger: strings.ToLower(trigger), tidbit: msg,
Operator: "reply", owner: user.Name,
FullText: msg, created: time.Now(),
Action: msg, accessed: time.Now(),
CreatedBy: user.Name, count: 0,
DateCreated: time.Now(),
LastAccessed: time.Now(),
AccessCount: 0,
} }
if err := p.Coll.Insert(fact); err != nil { if err := p.Coll.Insert(fact); err != nil {
log.Println("ERROR!!!!:", err) log.Println("ERROR!!!!:", err)
@ -165,7 +160,7 @@ func (p *RememberPlugin) Help(channel string, parts []string) {
// 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 (p *RememberPlugin) randQuote() string {
var quotes []Factoid var quotes []factoid
// todo: find anything with the word "quotes" in the trigger // todo: find anything with the word "quotes" in the trigger
query := bson.M{ query := bson.M{
"trigger": bson.M{ "trigger": bson.M{
@ -184,7 +179,7 @@ func (p *RememberPlugin) randQuote() string {
return "Sorry, I don't know any quotes." return "Sorry, I don't know any quotes."
} }
quote := quotes[rand.Intn(nquotes)] quote := quotes[rand.Intn(nquotes)]
return quote.FullText return quote.tidbit
} }
func (p *RememberPlugin) quoteTimer(channel string) { func (p *RememberPlugin) quoteTimer(channel string) {