diff --git a/bot/bot.go b/bot/bot.go index 7c5c1a6..873d361 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -46,6 +46,9 @@ type bot struct { // The entries to the bot's HTTP interface httpEndPoints map[string]string + + // filters registered by plugins + filters map[string]func(string) string } type Variable struct { @@ -77,6 +80,7 @@ func New(config *config.Config, connector Connector) Bot { logOut: logOut, version: config.Version, httpEndPoints: make(map[string]string), + filters: make(map[string]func(string) string), } bot.migrateDB() @@ -267,3 +271,8 @@ func (b *bot) checkAdmin(nick string) bool { } return false } + +// Register a text filter which every outgoing message is passed through +func (b *bot) RegisterFilter(name string, f func(string) string) { + b.filters[name] = f +} diff --git a/bot/handlers.go b/bot/handlers.go index 730f076..e28dd65 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -149,6 +149,10 @@ func (b *bot) Filter(message msg.Message, input string) string { panic(err) } + for _, f := range b.filters { + input = f(input) + } + varname := r.FindString(input) blacklist := make(map[string]bool) blacklist["$and"] = true diff --git a/bot/interfaces.go b/bot/interfaces.go index 12aa95d..23e1185 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -24,6 +24,7 @@ type Bot interface { LastMessage(string) (msg.Message, error) CheckAdmin(string) bool GetEmojiList() map[string]string + RegisterFilter(string, func(string) string) } type Connector interface { diff --git a/bot/mock.go b/bot/mock.go index d14ab2e..6b809f0 100644 --- a/bot/mock.go +++ b/bot/mock.go @@ -37,10 +37,11 @@ func (mb *MockBot) MsgReceived(msg msg.Message) {} func (mb *MockBot) EventReceived(msg msg.Message) {} func (mb *MockBot) Filter(msg msg.Message, s string) string { return "" } func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil } -func (mb *MockBot) CheckAdmin(nick string) bool { return false } +func (mb *MockBot) CheckAdmin(nick string) bool { return false } func (mb *MockBot) React(channel, reaction string, message msg.Message) {} func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } +func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} func NewMockBot() *MockBot { db, err := sqlx.Open("sqlite3_custom", ":memory:") diff --git a/config/config.go b/config/config.go index 948dd5f..9f29b79 100644 --- a/config/config.go +++ b/config/config.go @@ -100,6 +100,9 @@ type Config struct { PositiveReactions []string NegativeReactions []string } + Inventory struct { + Max int + } } func init() { diff --git a/main.go b/main.go index 4ee406c..f576aca 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "github.com/velour/catbase/plugins/emojifyme" "github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/first" + "github.com/velour/catbase/plugins/inventory" "github.com/velour/catbase/plugins/leftpad" "github.com/velour/catbase/plugins/reaction" "github.com/velour/catbase/plugins/reminder" @@ -67,6 +68,7 @@ func main() { b.AddHandler("reaction", reaction.New(b)) b.AddHandler("emojifyme", emojifyme.New(b)) b.AddHandler("twitch", twitch.New(b)) + b.AddHandler("inventory", inventory.New(b)) // catches anything left, will always return true b.AddHandler("factoid", fact.New(b)) diff --git a/plugins/inventory/inventory.go b/plugins/inventory/inventory.go new file mode 100644 index 0000000..abbaa35 --- /dev/null +++ b/plugins/inventory/inventory.go @@ -0,0 +1,238 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +// Package inventory contains the plugin that allows the bot to pad messages +// See the bucket RFC inventory: http://wiki.xkcd.com/irc/Bucket_inventory +package inventory + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/jmoiron/sqlx" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/config" +) + +type InventoryPlugin struct { + *sqlx.DB + bot bot.Bot + config *config.Config + r1, r2, r3, r4, r5 *regexp.Regexp +} + +// New creates a new InventoryPlugin with the Plugin interface +func New(bot bot.Bot) *InventoryPlugin { + config := bot.Config() + r1, err := regexp.Compile("take this (.+)") + checkerr(err) + r2, err := regexp.Compile("have a (.+)") + checkerr(err) + r3, err := regexp.Compile(fmt.Sprintf("puts (.+) in %s([^a-zA-Z].*)?", config.Nick)) + checkerr(err) + r4, err := regexp.Compile(fmt.Sprintf("gives %s (.+)", config.Nick)) + checkerr(err) + r5, err := regexp.Compile(fmt.Sprintf("gives (.+) to %s([^a-zA-Z].*)?", config.Nick)) + checkerr(err) + + p := InventoryPlugin{ + DB: bot.DB(), + bot: bot, + config: config, + r1: r1, r2: r2, r3: r3, r4: r4, r5: r5, + } + + bot.RegisterFilter("$item", p.itemFilter) + bot.RegisterFilter("$giveitem", p.giveItemFilter) + + _, err = p.DB.Exec(`create table if not exists inventory ( + item string primary key + );`) + + if err != nil { + log.Fatal(err) + } + + return &p +} + +func (p *InventoryPlugin) giveItemFilter(input string) string { + for strings.Contains(input, "$giveitem") { + item := p.random() + input = strings.Replace(input, "$giveitem", item, 1) + p.remove(item) + } + return input +} + +func (p *InventoryPlugin) itemFilter(input string) string { + for strings.Contains(input, "$item") { + input = strings.Replace(input, "$item", p.random(), 1) + } + return input +} + +func (p *InventoryPlugin) Message(message msg.Message) bool { + m := message.Body + log.Printf("inventory trying to read %+v", message) + if message.Command { + if strings.ToLower(m) == "inventory" { + items := p.getAll() + say := "I'm not holding anything" + if len(items) > 0 { + log.Printf("I think I have more than 0 items: %+v, len(items)=%d", items, len(items)) + say = fmt.Sprintf("I'm currently holding %s", strings.Join(items, ", ")) + } + p.bot.SendMessage(message.Channel, say) + return true + } + + // Bucket[:,] take this (.+) + // Bucket[:,] have a (.+) + if matches := p.r1.FindStringSubmatch(m); len(matches) > 0 { + log.Printf("Found item to add: %s", matches[1]) + return p.addItem(message, matches[1]) + } + if matches := p.r2.FindStringSubmatch(m); len(matches) > 0 { + log.Printf("Found item to add: %s", matches[1]) + return p.addItem(message, matches[1]) + } + } + if message.Action { + log.Println("Inventory found an action") + // * Randall puts (.+) in Bucket([^a-zA-Z].*)? + // * Randall gives Bucket (.+) + // * Randall gives (.+) to Bucket([^a-zA-Z].*)? + + if matches := p.r3.FindStringSubmatch(m); len(matches) > 0 { + log.Printf("Found item to add: %s", matches[1]) + return p.addItem(message, matches[1]) + } + if matches := p.r4.FindStringSubmatch(m); len(matches) > 0 { + log.Printf("Found item to add: %s", matches[1]) + return p.addItem(message, matches[1]) + } + if matches := p.r5.FindStringSubmatch(m); len(matches) > 0 { + log.Printf("Found item to add: %s", matches[1]) + return p.addItem(message, matches[1]) + } + } + return false +} + +func (p *InventoryPlugin) removeRandom() string { + var name string + err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( + &name, + ) + if err != nil { + log.Printf("Error finding random entry: %s", err) + return "IAMERROR" + } + _, err = p.Exec(`delete from inventory where item=?`, name) + if err != nil { + log.Printf("Error finding random entry: %s", err) + return "IAMERROR" + } + return name +} + +func (p *InventoryPlugin) count() int { + var output int + err := p.QueryRow(`select count(*) as count from inventory`).Scan(&output) + if err != nil { + log.Printf("Error checking for item: %s", err) + return -1 + } + return output +} + +func (p *InventoryPlugin) random() string { + var name string + err := p.QueryRow(`select item from inventory order by random() limit 1`).Scan( + &name, + ) + if err != nil { + log.Printf("Error finding random entry: %s", err) + return "IAMERROR" + } + return name +} + +func (p *InventoryPlugin) getAll() []string { + rows, err := p.Queryx(`select item from inventory`) + if err != nil { + log.Printf("Error getting all items: %s", err) + return []string{} + } + output := []string{} + for rows.Next() { + var item string + rows.Scan(&item) + output = append(output, item) + } + rows.Close() + return output +} + +func (p *InventoryPlugin) exists(i string) bool { + var output int + err := p.QueryRow(`select count(*) as count from inventory where item=?`, i).Scan(&output) + if err != nil { + log.Printf("Error checking for item: %s", err) + return false + } + return output > 0 +} + +func (p *InventoryPlugin) remove(i string) { + _, err := p.Exec(`delete from inventory where item=?`, i) + if err != nil { + log.Printf("Error inserting new inventory item: %s", err) + } +} + +func (p *InventoryPlugin) addItem(m msg.Message, i string) bool { + if p.exists(i) { + p.bot.SendMessage(m.Channel, fmt.Sprintf("I already have %s.", i)) + return true + } + var removed string + if p.count() > p.config.Inventory.Max { + removed = p.removeRandom() + } + _, err := p.Exec(`INSERT INTO inventory (item) values (?)`, i) + if err != nil { + log.Printf("Error inserting new inventory item: %s", err) + } + if removed != "" { + p.bot.SendAction(m.Channel, fmt.Sprintf("dropped %s and took %s from %s", removed, i, m.User.Name)) + } else { + p.bot.SendAction(m.Channel, fmt.Sprintf("takes %s from %s", i, m.User.Name)) + } + return true +} + +func checkerr(e error) { + if e != nil { + log.Println(e) + } +} + +func (p *InventoryPlugin) Event(e string, message msg.Message) bool { + return false +} + +func (p *InventoryPlugin) BotMessage(message msg.Message) bool { + return false +} + +func (p *InventoryPlugin) Help(e string, m []string) { +} + +func (p *InventoryPlugin) RegisterWeb() *string { + // nothing to register + return nil +} diff --git a/slack/slack.go b/slack/slack.go index 9c21b17..edd36fe 100644 --- a/slack/slack.go +++ b/slack/slack.go @@ -311,10 +311,7 @@ func (s *Slack) buildMessage(m slackMessage) msg.Message { isCmd, text := bot.IsCmd(s.config, text) - isAction := strings.HasPrefix(text, "/me ") - if isAction { - text = text[3:] - } + isAction := m.SubType == "me_message" u := s.getUser(m.User) if m.Username != "" {