2016-05-09 20:45:02 +00:00
|
|
|
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
|
|
|
|
|
|
|
|
package babbler
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"math/rand"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"github.com/velour/catbase/bot"
|
|
|
|
"github.com/velour/catbase/bot/msg"
|
2016-05-11 01:15:52 +00:00
|
|
|
"github.com/velour/catbase/bot/user"
|
|
|
|
"github.com/velour/catbase/config"
|
2016-05-09 20:45:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type BabblerPlugin struct {
|
2016-05-11 01:15:52 +00:00
|
|
|
Bot bot.Bot
|
|
|
|
db *sqlx.DB
|
|
|
|
config *config.Config
|
2016-05-09 20:45:02 +00:00
|
|
|
babblers map[string]*babbler
|
|
|
|
}
|
|
|
|
|
|
|
|
type babbler struct {
|
2016-05-11 01:15:52 +00:00
|
|
|
start *node
|
|
|
|
end *node
|
2016-05-09 20:45:02 +00:00
|
|
|
lookup map[string]*node
|
|
|
|
}
|
|
|
|
|
|
|
|
type node struct {
|
|
|
|
wordFrequency int
|
2016-05-11 01:15:52 +00:00
|
|
|
arcs map[string]*arc
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type arc struct {
|
|
|
|
transitionFrequency int
|
2016-05-11 01:15:52 +00:00
|
|
|
next *node
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func New(bot bot.Bot) *BabblerPlugin {
|
|
|
|
plugin := &BabblerPlugin{
|
2016-05-11 01:15:52 +00:00
|
|
|
Bot: bot,
|
|
|
|
db: bot.DB(),
|
|
|
|
config: bot.Config(),
|
2016-05-09 20:45:02 +00:00
|
|
|
babblers: map[string]*babbler{},
|
|
|
|
}
|
|
|
|
|
2016-05-11 01:15:52 +00:00
|
|
|
return plugin
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) makeBabbler(newUser user.User) {
|
|
|
|
name := newUser.Name
|
|
|
|
babbler, err := getMarkovChain(p.db, name)
|
2016-05-09 20:45:02 +00:00
|
|
|
if err == nil {
|
2016-05-11 01:15:52 +00:00
|
|
|
p.babblers[name] = babbler
|
2016-05-09 20:45:02 +00:00
|
|
|
} else {
|
2016-05-11 01:15:52 +00:00
|
|
|
p.babblers[name] = newBabbler()
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
2016-05-11 01:15:52 +00:00
|
|
|
}
|
2016-05-09 20:45:02 +00:00
|
|
|
|
2016-05-11 01:15:52 +00:00
|
|
|
func (p *BabblerPlugin) makeBabblers(newUser user.User) {
|
|
|
|
users := p.Bot.Who(p.config.MainChannel)
|
|
|
|
users = append(users, newUser)
|
|
|
|
for _, name := range p.config.Babbler.DefaultUsers {
|
|
|
|
users = append(users, user.New(name))
|
|
|
|
}
|
|
|
|
for _, u := range users {
|
|
|
|
p.makeBabbler(u)
|
|
|
|
}
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) Message(message msg.Message) bool {
|
2016-05-11 01:15:52 +00:00
|
|
|
if len(p.babblers) == 0 {
|
|
|
|
p.makeBabblers(*message.User)
|
|
|
|
} else if _, ok := p.babblers[message.User.Name]; !ok {
|
|
|
|
p.makeBabbler(*message.User)
|
|
|
|
}
|
|
|
|
|
2016-05-09 20:45:02 +00:00
|
|
|
lowercase := strings.ToLower(message.Body)
|
|
|
|
tokens := strings.Fields(lowercase)
|
2016-10-19 23:34:16 +00:00
|
|
|
numTokens := len(tokens)
|
2016-05-09 20:45:02 +00:00
|
|
|
|
2016-10-19 23:34:16 +00:00
|
|
|
if numTokens >= 2 && tokens[1] == "says" {
|
|
|
|
var saying string
|
|
|
|
if len(tokens) == 2 {
|
|
|
|
saying = p.babble(tokens[0])
|
|
|
|
} else {
|
2017-04-07 19:35:18 +00:00
|
|
|
saying = p.babbleSeed(tokens[0], tokens[2:])
|
2016-10-19 23:34:16 +00:00
|
|
|
}
|
2017-04-12 13:02:00 +00:00
|
|
|
if saying != "" {
|
2016-10-19 23:34:16 +00:00
|
|
|
p.Bot.SendMessage(message.Channel, saying)
|
2016-05-11 16:11:21 +00:00
|
|
|
}
|
|
|
|
return true
|
2016-05-11 17:07:16 +00:00
|
|
|
} else if len(tokens) == 4 && strings.Index(lowercase, "initialize babbler for ") == 0 {
|
|
|
|
who := tokens[3]
|
2016-05-09 20:45:02 +00:00
|
|
|
if _, ok := p.babblers[who]; !ok {
|
|
|
|
babbler, err := getMarkovChain(p.db, who)
|
|
|
|
if err == nil {
|
|
|
|
p.babblers[who] = babbler
|
|
|
|
} else {
|
|
|
|
p.babblers[who] = newBabbler()
|
|
|
|
}
|
2016-05-11 01:15:52 +00:00
|
|
|
p.Bot.SendMessage(message.Channel, "Okay.")
|
|
|
|
return true
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
2016-05-11 17:07:16 +00:00
|
|
|
} else if strings.Index(lowercase, "batch learn for ") == 0 {
|
|
|
|
who := tokens[3]
|
|
|
|
if _, ok := p.babblers[who]; !ok {
|
|
|
|
p.babblers[who] = newBabbler()
|
|
|
|
}
|
|
|
|
|
|
|
|
body := strings.Join(tokens[4:], " ")
|
|
|
|
body = strings.ToLower(body)
|
|
|
|
|
|
|
|
for _, a := range strings.Split(body, ".") {
|
|
|
|
for _, b := range strings.Split(a, "!") {
|
|
|
|
for _, c := range strings.Split(b, "?") {
|
|
|
|
for _, d := range strings.Split(c, "\n") {
|
|
|
|
trimmed := strings.TrimSpace(d)
|
|
|
|
if trimmed != "" {
|
|
|
|
addToMarkovChain(p.babblers[who], trimmed)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
p.Bot.SendMessage(message.Channel, "Phew that was tiring.")
|
|
|
|
return true
|
2016-05-26 15:06:22 +00:00
|
|
|
} else if len(tokens) == 5 && strings.Index(lowercase, "merge babbler") == 0 {
|
|
|
|
if tokens[3] != "into" {
|
|
|
|
p.Bot.SendMessage(message.Channel, "try using 'merge babbler [x] into [y]'")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
who := tokens[2]
|
|
|
|
into := tokens[4]
|
|
|
|
|
|
|
|
if who == into {
|
|
|
|
p.Bot.SendMessage(message.Channel, "Fuck off")
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
var whoBabbler *babbler
|
|
|
|
ok := false
|
|
|
|
if whoBabbler, ok = p.babblers[who]; !ok {
|
|
|
|
babbler, err := getMarkovChain(p.db, who)
|
|
|
|
if err == nil {
|
|
|
|
whoBabbler = babbler
|
|
|
|
} else {
|
|
|
|
whoBabbler = newBabbler()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := p.babblers[into]; !ok {
|
|
|
|
babbler, err := getMarkovChain(p.db, into)
|
|
|
|
if err == nil {
|
|
|
|
p.babblers[into] = babbler
|
|
|
|
} else {
|
|
|
|
p.babblers[into] = newBabbler()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
p.babblers[into].merge(whoBabbler, into, who)
|
|
|
|
|
|
|
|
p.Bot.SendMessage(message.Channel, "mooooiggged")
|
|
|
|
return true
|
2016-05-11 16:11:21 +00:00
|
|
|
} else {
|
|
|
|
addToMarkovChain(p.babblers[message.User.Name], lowercase)
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) Help(channel string, parts []string) {
|
2016-05-11 01:15:52 +00:00
|
|
|
p.Bot.SendMessage(channel, "initialize babbler for seabass\n\nseabass says")
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) Event(kind string, message msg.Message) bool {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) BotMessage(message msg.Message) bool {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) RegisterWeb() *string {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func addToMarkovChain(babble *babbler, phrase string) {
|
|
|
|
words := strings.Fields(strings.ToLower(phrase))
|
|
|
|
|
|
|
|
prev := babble.start
|
|
|
|
prev.wordFrequency++
|
|
|
|
for i := range words {
|
|
|
|
// has this word been seen before
|
|
|
|
if _, ok := babble.lookup[words[i]]; !ok {
|
|
|
|
babble.lookup[words[i]] = &node{
|
|
|
|
wordFrequency: 1,
|
2016-05-11 01:15:52 +00:00
|
|
|
arcs: map[string]*arc{},
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
babble.lookup[words[i]].wordFrequency++
|
|
|
|
}
|
|
|
|
|
|
|
|
// has this word been seen after the previous word before
|
|
|
|
if _, ok := prev.arcs[words[i]]; !ok {
|
|
|
|
prev.arcs[words[i]] = &arc{
|
|
|
|
transitionFrequency: 1,
|
2016-05-11 01:15:52 +00:00
|
|
|
next: babble.lookup[words[i]],
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
prev.arcs[words[i]].transitionFrequency++
|
|
|
|
}
|
|
|
|
prev = babble.lookup[words[i]]
|
|
|
|
}
|
|
|
|
|
|
|
|
// has this word ended a fact before
|
|
|
|
if _, ok := prev.arcs[""]; !ok {
|
|
|
|
prev.arcs[""] = &arc{
|
|
|
|
transitionFrequency: 1,
|
2016-05-11 01:15:52 +00:00
|
|
|
next: babble.end,
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
prev.arcs[""].transitionFrequency++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func newBabbler() *babbler {
|
2016-10-19 23:34:16 +00:00
|
|
|
start := &node{
|
|
|
|
wordFrequency: 0,
|
|
|
|
arcs: map[string]*arc{},
|
|
|
|
}
|
2016-05-11 01:15:52 +00:00
|
|
|
return &babbler{
|
2016-10-19 23:34:16 +00:00
|
|
|
start: start,
|
2016-05-11 01:15:52 +00:00
|
|
|
end: &node{
|
|
|
|
wordFrequency: 0,
|
|
|
|
arcs: map[string]*arc{},
|
|
|
|
},
|
2016-10-19 23:34:16 +00:00
|
|
|
lookup: map[string]*node{"": start},
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// this who string isn't escaped, just sooo, you know.
|
|
|
|
func getMarkovChain(db *sqlx.DB, who string) (*babbler, error) {
|
|
|
|
query := fmt.Sprintf(`select tidbit from factoid where fact like '%s quotes';`, who)
|
|
|
|
rows, err := db.Query(query)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
babble := newBabbler()
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
|
|
|
var tidbit string
|
|
|
|
err := rows.Scan(&tidbit)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
addToMarkovChain(babble, tidbit)
|
|
|
|
}
|
|
|
|
return babble, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *BabblerPlugin) babble(who string) string {
|
2017-04-07 19:35:18 +00:00
|
|
|
return p.babbleSeed(who, []string{""})
|
2016-10-19 23:34:16 +00:00
|
|
|
}
|
|
|
|
|
2017-04-07 19:35:18 +00:00
|
|
|
func (p *BabblerPlugin) babbleSeed(who string, seed []string) string {
|
2016-05-09 20:45:02 +00:00
|
|
|
if babbler, ok := p.babblers[who]; ok {
|
|
|
|
if len(babbler.start.arcs) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
2017-04-07 19:35:18 +00:00
|
|
|
|
|
|
|
words := seed
|
2016-10-19 23:34:16 +00:00
|
|
|
var cur *node
|
2017-04-07 19:35:18 +00:00
|
|
|
if cur, ok = babbler.lookup[words[0]]; !ok {
|
|
|
|
if len(words) == 1 {
|
|
|
|
return fmt.Sprintf("%s hasn't used the word '%s'", who, words[0])
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("%s hasn't used the phrase '%s'", who, strings.Join(words, " "))
|
|
|
|
}
|
2016-10-19 23:34:16 +00:00
|
|
|
}
|
2017-04-07 19:35:18 +00:00
|
|
|
|
|
|
|
for i := 1; i < len(words); i++ {
|
|
|
|
if arc, ok := cur.arcs[words[i]]; !ok {
|
|
|
|
if len(words) == 1 {
|
|
|
|
return fmt.Sprintf("%s hasn't used the word '%s'", who, words[0])
|
|
|
|
} else {
|
|
|
|
return fmt.Sprintf("%s hasn't used the phrase '%s'", who, strings.Join(words, " "))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cur = arc.next
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-09 20:45:02 +00:00
|
|
|
for cur != babbler.end {
|
|
|
|
which := rand.Intn(cur.wordFrequency)
|
|
|
|
sum := 0
|
|
|
|
for word, arc := range cur.arcs {
|
|
|
|
sum += arc.transitionFrequency
|
|
|
|
if sum > which {
|
|
|
|
words = append(words, word)
|
|
|
|
cur = arc.next
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-19 23:34:16 +00:00
|
|
|
return strings.TrimSpace(strings.Join(words, " "))
|
2016-05-09 20:45:02 +00:00
|
|
|
}
|
|
|
|
|
2017-04-12 13:02:00 +00:00
|
|
|
return ""
|
2016-05-11 01:15:52 +00:00
|
|
|
}
|
2016-05-26 15:06:22 +00:00
|
|
|
|
|
|
|
func (into *babbler) merge(other *babbler, intoName, otherName string) {
|
|
|
|
intoID := "<" + intoName + ">"
|
|
|
|
otherID := "<" + otherName + ">"
|
|
|
|
|
|
|
|
for nodeWord, myNode := range other.lookup {
|
|
|
|
if nodeWord == otherID {
|
|
|
|
nodeWord = intoID
|
|
|
|
}
|
|
|
|
|
|
|
|
//does this nodeWord exist yet?
|
|
|
|
if _, ok := into.lookup[nodeWord]; !ok {
|
|
|
|
into.lookup[nodeWord] = &node{
|
|
|
|
wordFrequency: myNode.wordFrequency,
|
|
|
|
arcs: map[string]*arc{},
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
into.lookup[nodeWord].wordFrequency += myNode.wordFrequency
|
|
|
|
}
|
|
|
|
|
|
|
|
for arcWord, myArc := range myNode.arcs {
|
|
|
|
if arcWord == otherID {
|
|
|
|
arcWord = intoID
|
|
|
|
}
|
|
|
|
|
|
|
|
if myArc.next == other.end {
|
|
|
|
if _, ok := into.lookup[nodeWord].arcs[arcWord]; !ok {
|
|
|
|
into.lookup[nodeWord].arcs[arcWord] = &arc{
|
|
|
|
transitionFrequency: myArc.transitionFrequency,
|
|
|
|
next: into.end,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
into.lookup[nodeWord].arcs[arcWord].transitionFrequency += myArc.transitionFrequency
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
//does the arcWord exist yet?
|
|
|
|
if _, ok := into.lookup[arcWord]; !ok {
|
|
|
|
into.lookup[arcWord] = &node{
|
|
|
|
wordFrequency: 0,
|
|
|
|
arcs: map[string]*arc{},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, ok := into.lookup[nodeWord].arcs[arcWord]; !ok {
|
|
|
|
into.lookup[nodeWord].arcs[arcWord] = &arc{
|
|
|
|
transitionFrequency: myArc.transitionFrequency,
|
|
|
|
next: into.lookup[arcWord],
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
into.lookup[nodeWord].arcs[arcWord].transitionFrequency += myArc.transitionFrequency
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
into.start.wordFrequency += other.start.wordFrequency
|
|
|
|
|
|
|
|
for startWord, startArc := range other.start.arcs {
|
|
|
|
if startWord == otherID {
|
|
|
|
startWord = intoID
|
|
|
|
}
|
|
|
|
if _, ok := into.start.arcs[startWord]; !ok {
|
|
|
|
into.start.arcs[startWord] = &arc{
|
|
|
|
transitionFrequency: startArc.transitionFrequency,
|
|
|
|
next: into.lookup[startWord],
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
into.start.arcs[startWord].transitionFrequency += startArc.transitionFrequency
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|