diff --git a/.gitignore b/.gitignore
index ba65788..dffcaf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,45 @@ vendor
.vscode/
*.code-workspace
*config.lua
+modd.conf
+
+
+# Created by https://www.gitignore.io/api/macos
+# Edit at https://www.gitignore.io/?templates=macos
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# End of https://www.gitignore.io/api/macos
+
+util/*/files
+util/*/files
+run.sh
+.idea
+logs
+util/files
diff --git a/README.md b/README.md
index 96d357f..bf5fede 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,7 @@
# CatBase
+[![Build Status](https://travis-ci.com/velour/catbase.svg?branch=master)](https://travis-ci.com/velour/catbase)
+
CatBase is a bot that trolls our little corner of the IRC world and keeps our friends laughing from time to time. Sometimes he makes us angry too. He is crafted as a clone of XKCD's Bucket bot, which learns from things he's told and regurgitates his knowledge to the various channels that he lives in. I've found in many such projects that randomness can often make bots feel much more alive than they are, so CatBase is a big experiment in how great randomness is.
## Getting Help
diff --git a/bot/bot.go b/bot/bot.go
index 777229c..4d0c1a0 100644
--- a/bot/bot.go
+++ b/bot/bot.go
@@ -3,13 +3,15 @@
package bot
import (
- "database/sql"
- "html/template"
- "log"
+ "fmt"
+ "math/rand"
"net/http"
+ "reflect"
"strings"
+ "time"
"github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/msglog"
"github.com/velour/catbase/bot/user"
@@ -20,7 +22,7 @@ import (
type bot struct {
// Each plugin must be registered in our plugins handler. To come: a map so that this
// will allow plugins to respond to specific kinds of events
- plugins map[string]Handler
+ plugins map[string]Plugin
pluginOrdering []string
// Users holds information about all of our friends
@@ -32,30 +34,33 @@ type bot struct {
conn Connector
- // SQL DB
- // TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that
- // the select/update/etc statements could be simplified with struct
- // marshalling.
- db *sqlx.DB
- dbVersion int64
-
logIn chan msg.Message
logOut chan msg.Messages
version string
// The entries to the bot's HTTP interface
- httpEndPoints map[string]string
+ httpEndPoints []EndPoint
// filters registered by plugins
filters map[string]func(string) string
+
+ callbacks CallbackMap
+
+ password string
+ passwordCreated time.Time
}
+type EndPoint struct {
+ Name, URL string
+}
+
+// Variable represents a $var replacement
type Variable struct {
Variable, Value string
}
-// Newbot creates a bot for a given connection and set of handlers.
+// New creates a bot for a given connection and set of handlers.
func New(config *config.Config, connector Connector) Bot {
logIn := make(chan msg.Message)
logOut := make(chan msg.Messages)
@@ -63,94 +68,69 @@ func New(config *config.Config, connector Connector) Bot {
msglog.RunNew(logIn, logOut)
users := []user.User{
- user.User{
- Name: config.Nick,
+ {
+ Name: config.Get("Nick", "bot"),
},
}
bot := &bot{
config: config,
- plugins: make(map[string]Handler),
+ plugins: make(map[string]Plugin),
pluginOrdering: make([]string, 0),
conn: connector,
users: users,
me: users[0],
- db: config.DBConn,
logIn: logIn,
logOut: logOut,
- version: config.Version,
- httpEndPoints: make(map[string]string),
+ httpEndPoints: make([]EndPoint, 0),
filters: make(map[string]func(string) string),
+ callbacks: make(CallbackMap),
}
bot.migrateDB()
http.HandleFunc("/", bot.serveRoot)
- if config.HttpAddr == "" {
- config.HttpAddr = "127.0.0.1:1337"
- }
- go http.ListenAndServe(config.HttpAddr, nil)
- connector.RegisterMessageReceived(bot.MsgReceived)
- connector.RegisterEventReceived(bot.EventReceived)
- connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived)
+ connector.RegisterEvent(bot.Receive)
return bot
}
+func (b *bot) DefaultConnector() Connector {
+ return b.conn
+}
+
+func (b *bot) WhoAmI() string {
+ return b.me.Name
+}
+
// Config gets the configuration that the bot is using
func (b *bot) Config() *config.Config {
return b.config
}
-func (b *bot) DBVersion() int64 {
- return b.dbVersion
-}
-
func (b *bot) DB() *sqlx.DB {
- return b.db
+ return b.config.DB
}
// Create any tables if necessary based on version of DB
// Plugins should create their own tables, these are only for official bot stuff
// Note: This does not return an error. Database issues are all fatal at this stage.
func (b *bot) migrateDB() {
- _, err := b.db.Exec(`create table if not exists version (version integer);`)
- if err != nil {
- log.Fatal("Initial DB migration create version table: ", err)
- }
- var version sql.NullInt64
- err = b.db.QueryRow("select max(version) from version").Scan(&version)
- if err != nil {
- log.Fatal("Initial DB migration get version: ", err)
- }
- if version.Valid {
- b.dbVersion = version.Int64
- log.Printf("Database version: %v\n", b.dbVersion)
- } else {
- log.Printf("No versions, we're the first!.")
- _, err := b.db.Exec(`insert into version (version) values (1)`)
- if err != nil {
- log.Fatal("Initial DB migration insert: ", err)
- }
- }
-
- if _, err := b.db.Exec(`create table if not exists variables (
+ if _, err := b.DB().Exec(`create table if not exists variables (
id integer primary key,
name string,
value string
);`); err != nil {
- log.Fatal("Initial DB migration create variables table: ", err)
+ log.Fatal().Err(err).Msgf("Initial DB migration create variables table")
}
}
// Adds a constructed handler to the bots handlers list
-func (b *bot) AddHandler(name string, h Handler) {
+func (b *bot) AddPlugin(h Plugin) {
+ name := reflect.TypeOf(h).String()
b.plugins[name] = h
b.pluginOrdering = append(b.pluginOrdering, name)
- if entry := h.RegisterWeb(); entry != nil {
- b.httpEndPoints[name] = *entry
- }
}
func (b *bot) Who(channel string) []user.User {
@@ -162,50 +142,14 @@ func (b *bot) Who(channel string) []user.User {
return users
}
-var rootIndex string = `
-
-
-
- Factoids
-
-
-
- {{if .EndPoints}}
-
-
-
-
- Plugin
-
-
-
-
- {{range $key, $value := .EndPoints}}
-
- {{$key}}
-
- {{end}}
-
-
-
- {{end}}
-
-`
-
-func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) {
- context := make(map[string]interface{})
- context["EndPoints"] = b.httpEndPoints
- t, err := template.New("rootIndex").Parse(rootIndex)
- if err != nil {
- log.Println(err)
- }
- t.Execute(w, context)
-}
-
-// Checks if message is a command and returns its curtailed version
+// IsCmd checks if message is a command and returns its curtailed version
func IsCmd(c *config.Config, message string) (bool, string) {
- cmdcs := c.CommandChar
- botnick := strings.ToLower(c.Nick)
+ cmdcs := c.GetArray("CommandChar", []string{"!"})
+ botnick := strings.ToLower(c.Get("Nick", "bot"))
+ if botnick == "" {
+ log.Fatal().
+ Msgf(`You must run catbase -set nick -val `)
+ }
iscmd := false
lowerMessage := strings.ToLower(message)
@@ -237,7 +181,7 @@ func IsCmd(c *config.Config, message string) (bool, string) {
}
func (b *bot) CheckAdmin(nick string) bool {
- for _, u := range b.Config().Admins {
+ for _, u := range b.Config().GetArray("Admins", []string{}) {
if nick == u {
return true
}
@@ -265,7 +209,7 @@ func (b *bot) NewUser(nick string) *user.User {
}
func (b *bot) checkAdmin(nick string) bool {
- for _, u := range b.Config().Admins {
+ for _, u := range b.Config().GetArray("Admins", []string{}) {
if nick == u {
return true
}
@@ -277,3 +221,31 @@ func (b *bot) checkAdmin(nick string) bool {
func (b *bot) RegisterFilter(name string, f func(string) string) {
b.filters[name] = f
}
+
+// Register a callback
+func (b *bot) Register(p Plugin, kind Kind, cb Callback) {
+ t := reflect.TypeOf(p).String()
+ if _, ok := b.callbacks[t]; !ok {
+ b.callbacks[t] = make(map[Kind][]Callback)
+ }
+ if _, ok := b.callbacks[t][kind]; !ok {
+ b.callbacks[t][kind] = []Callback{}
+ }
+ b.callbacks[t][kind] = append(b.callbacks[t][kind], cb)
+}
+
+func (b *bot) RegisterWeb(root, name string) {
+ b.httpEndPoints = append(b.httpEndPoints, EndPoint{name, root})
+}
+
+func (b *bot) GetPassword() string {
+ if b.passwordCreated.Before(time.Now().Add(-24 * time.Hour)) {
+ adjs := b.config.GetArray("bot.passwordAdjectives", []string{"very"})
+ nouns := b.config.GetArray("bot.passwordNouns", []string{"noun"})
+ verbs := b.config.GetArray("bot.passwordVerbs", []string{"do"})
+ a, n, v := adjs[rand.Intn(len(adjs))], nouns[rand.Intn(len(nouns))], verbs[rand.Intn(len(verbs))]
+ b.passwordCreated = time.Now()
+ b.password = fmt.Sprintf("%s-%s-%s", a, n, v)
+ }
+ return b.password
+}
diff --git a/bot/handlers.go b/bot/handlers.go
index cd01bb0..0a1a3e9 100644
--- a/bot/handlers.go
+++ b/bot/handlers.go
@@ -6,86 +6,55 @@ import (
"database/sql"
"errors"
"fmt"
- "log"
"math/rand"
+ "reflect"
"regexp"
"strconv"
"strings"
"time"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg"
)
-// Handles incomming PRIVMSG requests
-func (b *bot) MsgReceived(msg msg.Message) {
- log.Println("Received message: ", msg)
+func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interface{}) bool {
+ log.Debug().
+ Interface("msg", msg).
+ Msg("Received event")
// msg := b.buildMessage(client, inMsg)
// do need to look up user and fix it
- if strings.HasPrefix(msg.Body, "help ") && msg.Command {
+ if kind == Message && strings.HasPrefix(msg.Body, "help") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body))
- b.checkHelp(msg.Channel, parts)
+ b.checkHelp(conn, msg.Channel, parts)
+ log.Debug().Msg("Handled a help, returning")
goto RET
}
for _, name := range b.pluginOrdering {
- p := b.plugins[name]
- if p.Message(msg) {
- break
+ if b.runCallback(conn, b.plugins[name], kind, msg, args...) {
+ goto RET
}
}
RET:
b.logIn <- msg
- return
+ return true
}
-// Handle incoming events
-func (b *bot) EventReceived(msg msg.Message) {
- log.Println("Received event: ", msg)
- //msg := b.buildMessage(conn, inMsg)
- for _, name := range b.pluginOrdering {
- p := b.plugins[name]
- if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body
- break
+func (b *bot) runCallback(conn Connector, plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool {
+ t := reflect.TypeOf(plugin).String()
+ for _, cb := range b.callbacks[t][evt] {
+ if cb(conn, evt, message, args...) {
+ return true
}
}
+ return false
}
-// Handle incoming replys
-func (b *bot) ReplyMsgReceived(msg msg.Message, identifier string) {
- log.Println("Received message: ", msg)
-
- for _, name := range b.pluginOrdering {
- p := b.plugins[name]
- if p.ReplyMessage(msg, identifier) {
- break
- }
- }
-}
-
-func (b *bot) SendMessage(channel, message string) string {
- return b.conn.SendMessage(channel, message)
-}
-
-func (b *bot) SendAction(channel, message string) string {
- return b.conn.SendAction(channel, message)
-}
-
-func (b *bot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
- return b.conn.ReplyToMessageIdentifier(channel, message, identifier)
-}
-
-func (b *bot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
- return b.conn.ReplyToMessage(channel, message, replyTo)
-}
-
-func (b *bot) React(channel, reaction string, message msg.Message) bool {
- return b.conn.React(channel, reaction, message)
-}
-
-func (b *bot) Edit(channel, newMessage, identifier string) bool {
- return b.conn.Edit(channel, newMessage, identifier)
+// Send a message to the connection
+func (b *bot) Send(conn Connector, kind Kind, args ...interface{}) (string, error) {
+ return conn.Send(kind, args...)
}
func (b *bot) GetEmojiList() map[string]string {
@@ -93,31 +62,38 @@ func (b *bot) GetEmojiList() map[string]string {
}
// Checks to see if the user is asking for help, returns true if so and handles the situation.
-func (b *bot) checkHelp(channel string, parts []string) {
+func (b *bot) checkHelp(conn Connector, channel string, parts []string) {
if len(parts) == 1 {
// just print out a list of help topics
topics := "Help topics: about variables"
- for name, _ := range b.plugins {
+ for name := range b.plugins {
+ name = strings.Split(strings.TrimPrefix(name, "*"), ".")[0]
topics = fmt.Sprintf("%s, %s", topics, name)
}
- b.SendMessage(channel, topics)
+ b.Send(conn, Message, channel, topics)
} else {
// trigger the proper plugin's help response
if parts[1] == "about" {
- b.Help(channel, parts)
+ b.Help(conn, channel, parts)
return
}
if parts[1] == "variables" {
- b.listVars(channel, parts)
+ b.listVars(conn, channel, parts)
return
}
- plugin := b.plugins[parts[1]]
- if plugin != nil {
- plugin.Help(channel, parts)
- } else {
- msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", parts[1])
- b.SendMessage(channel, msg)
+ for name, plugin := range b.plugins {
+ if strings.HasPrefix(name, "*"+parts[1]) {
+ if b.runCallback(conn, plugin, Help, msg.Message{Channel: channel}, channel, parts) {
+ return
+ } else {
+ msg := fmt.Sprintf("I'm sorry, I don't know how to help you with %s.", parts[1])
+ b.Send(conn, Message, channel, msg)
+ return
+ }
+ }
}
+ msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", strings.Join(parts, " "))
+ b.Send(conn, Message, channel, msg)
}
}
@@ -192,38 +168,38 @@ func (b *bot) Filter(message msg.Message, input string) string {
func (b *bot) getVar(varName string) (string, error) {
var text string
- err := b.db.Get(&text, `select value from variables where name=? order by random() limit 1`, varName)
+ err := b.DB().Get(&text, `select value from variables where name=? order by random() limit 1`, varName)
switch {
case err == sql.ErrNoRows:
return "", fmt.Errorf("No factoid found")
case err != nil:
- log.Fatal("getVar error: ", err)
+ log.Fatal().Err(err).Msg("getVar error")
}
return text, nil
}
-func (b *bot) listVars(channel string, parts []string) {
+func (b *bot) listVars(conn Connector, channel string, parts []string) {
var variables []string
- err := b.db.Select(&variables, `select name from variables group by name`)
+ err := b.DB().Select(&variables, `select name from variables group by name`)
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
msg := "I know: $who, $someone, $digit, $nonzero"
if len(variables) > 0 {
msg += ", " + strings.Join(variables, ", ")
}
- b.SendMessage(channel, msg)
+ b.Send(conn, Message, channel, msg)
}
-func (b *bot) Help(channel string, parts []string) {
+func (b *bot) Help(conn Connector, channel string, parts []string) {
msg := fmt.Sprintf("Hi, I'm based on godeepintir version %s. I'm written in Go, and you "+
"can find my source code on the internet here: "+
"http://github.com/velour/catbase", b.version)
- b.SendMessage(channel, msg)
+ b.Send(conn, Message, channel, msg)
}
// Send our own musings to the plugins
-func (b *bot) selfSaid(channel, message string, action bool) {
+func (b *bot) selfSaid(conn Connector, channel, message string, action bool) {
msg := msg.Message{
User: &b.me, // hack
Channel: channel,
@@ -236,9 +212,8 @@ func (b *bot) selfSaid(channel, message string, action bool) {
}
for _, name := range b.pluginOrdering {
- p := b.plugins[name]
- if p.BotMessage(msg) {
- break
+ if b.runCallback(conn, b.plugins[name], SelfMessage, msg) {
+ return
}
}
}
diff --git a/bot/interfaces.go b/bot/interfaces.go
index 2780d29..d9f5b62 100644
--- a/bot/interfaces.go
+++ b/bot/interfaces.go
@@ -9,51 +9,80 @@ import (
"github.com/velour/catbase/config"
)
+const (
+ _ = iota
+
+ // Message any standard chat
+ Message
+ // Reply something containing a message reference
+ Reply
+ // Action any /me action
+ Action
+ // Reaction Icon reaction if service supports it
+ Reaction
+ // Edit message ref'd new message to replace
+ Edit
+ // Not sure what event is
+ Event
+ // Help is used when the bot help system is triggered
+ Help
+ // SelfMessage triggers when the bot is sending a message
+ SelfMessage
+)
+
+type ImageAttachment struct {
+ URL string
+ AltTxt string
+}
+
+type Kind int
+type Callback func(Connector, Kind, msg.Message, ...interface{}) bool
+type CallbackMap map[string]map[Kind][]Callback
+
+// Bot interface serves to allow mocking of the actual bot
type Bot interface {
+ // Config allows access to the bot's configuration system
Config() *config.Config
- DBVersion() int64
+ // DB gives access to the current database
DB() *sqlx.DB
+ // Who lists users in a particular channel
Who(string) []user.User
- AddHandler(string, Handler)
- SendMessage(string, string) string
- SendAction(string, string) string
- ReplyToMessageIdentifier(string, string, string) (string, bool)
- ReplyToMessage(string, string, msg.Message) (string, bool)
- React(string, string, msg.Message) bool
- Edit(string, string, string) bool
- MsgReceived(msg.Message)
- ReplyMsgReceived(msg.Message, string)
- EventReceived(msg.Message)
+ // WhoAmI gives a nick for the bot
+ WhoAmI() string
+ // AddPlugin registers a new plugin handler
+ AddPlugin(Plugin)
+ // First arg should be one of bot.Message/Reply/Action/etc
+ Send(Connector, Kind, ...interface{}) (string, error)
+ // First arg should be one of bot.Message/Reply/Action/etc
+ Receive(Connector, Kind, msg.Message, ...interface{}) bool
+ // Register a callback
+ Register(Plugin, Kind, Callback)
+
Filter(msg.Message, string) string
LastMessage(string) (msg.Message, error)
+
CheckAdmin(string) bool
GetEmojiList() map[string]string
RegisterFilter(string, func(string) string)
+ RegisterWeb(string, string)
+ DefaultConnector() Connector
+ GetWebNavigation() []EndPoint
+ GetPassword() string
}
+// Connector represents a server connection to a chat service
type Connector interface {
- RegisterEventReceived(func(message msg.Message))
- RegisterMessageReceived(func(message msg.Message))
- RegisterReplyMessageReceived(func(msg.Message, string))
+ RegisterEvent(Callback)
+
+ Send(Kind, ...interface{}) (string, error)
- SendMessage(channel, message string) string
- SendAction(channel, message string) string
- ReplyToMessageIdentifier(string, string, string) (string, bool)
- ReplyToMessage(string, string, msg.Message) (string, bool)
- React(string, string, msg.Message) bool
- Edit(string, string, string) bool
GetEmojiList() map[string]string
Serve() error
Who(string) []string
}
-// Interface used for compatibility with the Plugin interface
-type Handler interface {
- Message(message msg.Message) bool
- Event(kind string, message msg.Message) bool
- ReplyMessage(msg.Message, string) bool
- BotMessage(message msg.Message) bool
- Help(channel string, parts []string)
- RegisterWeb() *string
+// Plugin interface used for compatibility with the Plugin interface
+// Uhh it turned empty, but we're still using it to ID plugins
+type Plugin interface {
}
diff --git a/bot/mock.go b/bot/mock.go
index 7d6c6e6..6d7fb70 100644
--- a/bot/mock.go
+++ b/bot/mock.go
@@ -4,11 +4,12 @@ package bot
import (
"fmt"
- "log"
+ "net/http"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
"github.com/stretchr/testify/mock"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
@@ -19,85 +20,94 @@ type MockBot struct {
mock.Mock
db *sqlx.DB
- Cfg config.Config
+ Cfg *config.Config
- Messages []string
- Actions []string
+ Messages []string
+ Actions []string
+ Reactions []string
}
-func (mb *MockBot) Config() *config.Config { return &mb.Cfg }
-func (mb *MockBot) DBVersion() int64 { return 1 }
-func (mb *MockBot) DB() *sqlx.DB { return mb.db }
-func (mb *MockBot) Conn() Connector { return nil }
-func (mb *MockBot) Who(string) []user.User { return []user.User{} }
-func (mb *MockBot) AddHandler(name string, f Handler) {}
-func (mb *MockBot) SendMessage(ch string, msg string) string {
- mb.Messages = append(mb.Messages, msg)
- return fmt.Sprintf("m-%d", len(mb.Actions)-1)
+func (mb *MockBot) Config() *config.Config { return mb.Cfg }
+func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB }
+func (mb *MockBot) Who(string) []user.User { return []user.User{} }
+func (mb *MockBot) WhoAmI() string { return "tester" }
+func (mb *MockBot) DefaultConnector() Connector { return nil }
+func (mb *MockBot) GetPassword() string { return "12345" }
+func (mb *MockBot) Send(c Connector, kind Kind, args ...interface{}) (string, error) {
+ switch kind {
+ case Message:
+ mb.Messages = append(mb.Messages, args[1].(string))
+ return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
+ case Action:
+ mb.Actions = append(mb.Actions, args[1].(string))
+ return fmt.Sprintf("a-%d", len(mb.Actions)-1), nil
+ case Edit:
+ ch, m, id := args[0].(string), args[1].(string), args[2].(string)
+ return mb.edit(c, ch, m, id)
+ case Reaction:
+ ch, re, msg := args[0].(string), args[1].(string), args[2].(msg.Message)
+ return mb.react(c, ch, re, msg)
+ }
+ return "ERR", fmt.Errorf("Mesasge type unhandled")
}
-func (mb *MockBot) SendAction(ch string, msg string) string {
- mb.Actions = append(mb.Actions, msg)
- return fmt.Sprintf("a-%d", len(mb.Actions)-1)
+func (mb *MockBot) AddPlugin(f Plugin) {}
+func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {}
+func (mb *MockBot) RegisterWeb(_, _ string) {}
+func (mb *MockBot) GetWebNavigation() []EndPoint { return nil }
+func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...interface{}) bool {
+ return false
}
-func (mb *MockBot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
- return "", false
-}
-func (mb *MockBot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
- return "", false
-}
-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) Filter(msg msg.Message, s string) string { return s }
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) React(channel, reaction string, message msg.Message) bool { return false }
+func (mb *MockBot) react(c Connector, channel, reaction string, message msg.Message) (string, error) {
+ mb.Reactions = append(mb.Reactions, reaction)
+ return "", nil
+}
-func (mb *MockBot) Edit(channel, newMessage, identifier string) bool {
+func (mb *MockBot) edit(c Connector, channel, newMessage, identifier string) (string, error) {
isMessage := identifier[0] == 'm'
if !isMessage && identifier[0] != 'a' {
- log.Printf("failed to parse identifier: %s", identifier)
- return false
+ err := fmt.Errorf("failed to parse identifier: %s", identifier)
+ log.Error().Err(err)
+ return "", err
}
index, err := strconv.Atoi(strings.Split(identifier, "-")[1])
if err != nil {
- log.Printf("failed to parse identifier: %s", identifier)
- return false
+ err := fmt.Errorf("failed to parse identifier: %s", identifier)
+ log.Error().Err(err)
+ return "", err
}
if isMessage {
if index < len(mb.Messages) {
mb.Messages[index] = newMessage
} else {
- return false
+ return "", fmt.Errorf("No message")
}
} else {
if index < len(mb.Actions) {
mb.Actions[index] = newMessage
} else {
- return false
+ return "", fmt.Errorf("No action")
}
}
- return true
-}
-
-func (mb *MockBot) ReplyMsgReceived(msg.Message, string) {
-
+ return "", nil
}
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:")
- if err != nil {
- log.Fatal("Failed to open database:", err)
- }
+ cfg := config.ReadConfig("file::memory:?mode=memory&cache=shared")
b := MockBot{
- db: db,
+ Cfg: cfg,
Messages: make([]string, 0),
Actions: make([]string, 0),
}
+ // If any plugin registered a route, we need to reset those before any new test
+ http.DefaultServeMux = new(http.ServeMux)
return &b
}
diff --git a/bot/msg/message.go b/bot/msg/message.go
index 78a40f0..6d3712c 100644
--- a/bot/msg/message.go
+++ b/bot/msg/message.go
@@ -12,9 +12,14 @@ type Log Messages
type Messages []Message
type Message struct {
- User *user.User
- Channel, Body string
- Raw string
+ User *user.User
+ // With Slack, channel is the ID of a channel
+ Channel string
+ // With slack, channelName is the nice name of a channel
+ ChannelName string
+ Body string
+ IsIM bool
+ Raw interface{}
Command bool
Action bool
Time time.Time
diff --git a/bot/web.go b/bot/web.go
new file mode 100644
index 0000000..b8e52ed
--- /dev/null
+++ b/bot/web.go
@@ -0,0 +1,73 @@
+package bot
+
+import (
+ "html/template"
+ "net/http"
+ "strings"
+)
+
+func (b *bot) serveRoot(w http.ResponseWriter, r *http.Request) {
+ context := make(map[string]interface{})
+ context["Nav"] = b.GetWebNavigation()
+ t := template.Must(template.New("rootIndex").Parse(rootIndex))
+ t.Execute(w, context)
+}
+
+// GetWebNavigation returns a list of bootstrap-vue links
+// The parent is not included so each page may display it as
+// best fits
+func (b *bot) GetWebNavigation() []EndPoint {
+ endpoints := b.httpEndPoints
+ moreEndpoints := b.config.GetArray("bot.links", []string{})
+ for _, e := range moreEndpoints {
+ link := strings.SplitN(e, ":", 2)
+ if len(link) != 2 {
+ continue
+ }
+ endpoints = append(endpoints, EndPoint{link[0], link[1]})
+ }
+ return endpoints
+}
+
+var rootIndex = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Factoids
+
+
+
+
+
+ catbase
+
+ {{ "{{ item.Name }}" }}
+
+
+
+
+
+
+
+`
diff --git a/config/config.go b/config/config.go
index a16e00c..ee767d2 100644
--- a/config/config.go
+++ b/config/config.go
@@ -5,112 +5,119 @@ package config
import (
"database/sql"
"fmt"
- "log"
+ "os"
"regexp"
+ "strconv"
+ "strings"
"github.com/jmoiron/sqlx"
- sqlite3 "github.com/mattn/go-sqlite3"
- "github.com/yuin/gluamapper"
- lua "github.com/yuin/gopher-lua"
+ "github.com/mattn/go-sqlite3"
+ "github.com/rs/zerolog/log"
)
// Config stores any system-wide startup information that cannot be easily configured via
// the database
type Config struct {
- DBConn *sqlx.DB
+ *sqlx.DB
- DB struct {
- File string
- Name string
- Server string
+ DBFile string
+}
+
+// GetFloat64 returns the config value for a string key
+// It will first look in the env vars for the key
+// It will check the DB for the key if an env DNE
+// Finally, it will return a zero value if the key does not exist
+// It will attempt to convert the value to a float64 if it exists
+func (c *Config) GetFloat64(key string, fallback float64) float64 {
+ f, err := strconv.ParseFloat(c.GetString(key, fmt.Sprintf("%f", fallback)), 64)
+ if err != nil {
+ return 0.0
}
- Channels []string
- MainChannel string
- Plugins []string
- Type string
- Irc struct {
- Server, Pass string
+ return f
+}
+
+// GetInt returns the config value for a string key
+// It will first look in the env vars for the key
+// It will check the DB for the key if an env DNE
+// Finally, it will return a zero value if the key does not exist
+// It will attempt to convert the value to an int if it exists
+func (c *Config) GetInt(key string, fallback int) int {
+ i, err := strconv.Atoi(c.GetString(key, strconv.Itoa(fallback)))
+ if err != nil {
+ return 0
}
- Slack struct {
- Token string
+ return i
+}
+
+// Get is a shortcut for GetString
+func (c *Config) Get(key, fallback string) string {
+ return c.GetString(key, fallback)
+}
+
+func envkey(key string) string {
+ key = strings.ToUpper(key)
+ key = strings.Replace(key, ".", "", -1)
+ return key
+}
+
+// GetString returns the config value for a string key
+// It will first look in the env vars for the key
+// It will check the DB for the key if an env DNE
+// Finally, it will return a zero value if the key does not exist
+// It will convert the value to a string if it exists
+func (c *Config) GetString(key, fallback string) string {
+ key = strings.ToLower(key)
+ if v, found := os.LookupEnv(envkey(key)); found {
+ return v
}
- Nick string
- IconURL string
- FullName string
- Version string
- CommandChar []string
- RatePerSec float64
- LogLength int
- Admins []string
- HttpAddr string
- Untappd struct {
- Token string
- Freq int
- Channels []string
+ var configValue string
+ q := `select value from config where key=?`
+ err := c.DB.Get(&configValue, q, key)
+ if err != nil {
+ log.Debug().Msgf("WARN: Key %s is empty", key)
+ return fallback
}
- Twitch struct {
- Freq int
- Users map[string][]string //channel -> usernames
- ClientID string
- Authorization string
+ return configValue
+}
+
+// GetArray returns the string slice config value for a string key
+// It will first look in the env vars for the key with ;; separated values
+// Look, I'm too lazy to do parsing to ensure that a comma is what the user meant
+// It will check the DB for the key if an env DNE
+// Finally, it will return a zero value if the key does not exist
+// This will do no conversion.
+func (c *Config) GetArray(key string, fallback []string) []string {
+ val := c.GetString(key, "")
+ if val == "" {
+ return fallback
}
- EnforceNicks bool
- WelcomeMsgs []string
- TwitterConsumerKey string
- TwitterConsumerSecret string
- TwitterUserKey string
- TwitterUserSecret string
- BadMsgs []string
- Bad struct {
- Msgs []string
- Nicks []string
- Hosts []string
+ return strings.Split(val, ";;")
+}
+
+// Set changes the value for a configuration in the database
+// Note, this is always a string. Use the SetArray for an array helper
+func (c *Config) Set(key, value string) error {
+ key = strings.ToLower(key)
+ q := `insert into config (key,value) values (?, ?)
+ on conflict(key) do update set value=?;`
+ tx, err := c.Begin()
+ if err != nil {
+ return err
}
- Your struct {
- MaxLength int
- Replacements []Replacement
+ _, err = tx.Exec(q, key, value, value)
+ if err != nil {
+ return err
}
- LeftPad struct {
- MaxLen int
- Who string
- }
- Factoid struct {
- MinLen int
- QuoteChance float64
- QuoteTime int
- StartupFact string
- }
- Babbler struct {
- DefaultUsers []string
- }
- Reminder struct {
- MaxBatchAdd int
- }
- Stats struct {
- DBPath string
- Sightings []string
- }
- Emojify struct {
- Chance float64
- Scoreless []string
- }
- Reaction struct {
- GeneralChance float64
- HarrassChance float64
- NegativeHarrassmentMultiplier int
- HarrassList []string
- PositiveReactions []string
- NegativeReactions []string
- }
- Inventory struct {
- Max int
- }
- Sisyphus struct {
- MinDecrement int
- MaxDecrement int
- MinPush int
- MaxPush int
+ err = tx.Commit()
+ if err != nil {
+ return err
}
+ return nil
+}
+
+func (c *Config) SetArray(key string, values []string) error {
+ vals := strings.Join(values, ";;")
+ return c.Set(key, vals)
}
func init() {
@@ -125,38 +132,31 @@ func init() {
})
}
-type Replacement struct {
- This string
- That string
- Frequency float64
-}
-
// Readconfig loads the config data out of a JSON file located in cfile
-func Readconfig(version, cfile string) *Config {
- fmt.Printf("Using %s as config file.\n", cfile)
- L := lua.NewState()
- if err := L.DoFile(cfile); err != nil {
- panic(err)
+func ReadConfig(dbpath string) *Config {
+ if dbpath == "" {
+ dbpath = "catbase.db"
}
+ log.Info().Msgf("Using %s as database file.\n", dbpath)
- var c Config
- if err := gluamapper.Map(L.GetGlobal("config").(*lua.LTable), &c); err != nil {
- panic(err)
- }
-
- c.Version = version
-
- if c.Type == "" {
- c.Type = "irc"
- }
-
- fmt.Printf("godeepintir version %s running.\n", c.Version)
-
- sqlDB, err := sqlx.Open("sqlite3_custom", c.DB.File)
+ sqlDB, err := sqlx.Open("sqlite3_custom", dbpath)
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
- c.DBConn = sqlDB
+ c := Config{
+ DBFile: dbpath,
+ }
+ c.DB = sqlDB
+
+ if _, err := c.Exec(`create table if not exists config (
+ key string,
+ value string,
+ primary key (key)
+ );`); err != nil {
+ panic(err)
+ }
+
+ log.Info().Msgf("catbase is running.")
return &c
}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..3bdfb59
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,23 @@
+package config
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestSetGet(t *testing.T) {
+ cfg := ReadConfig(":memory:")
+ expected := "value"
+ cfg.Set("test", expected)
+ actual := cfg.Get("test", "NOPE")
+ assert.Equal(t, expected, actual, "Config did not store values")
+}
+
+func TestSetGetArray(t *testing.T) {
+ cfg := ReadConfig(":memory:")
+ expected := []string{"a", "b", "c"}
+ cfg.SetArray("test", expected)
+ actual := cfg.GetArray("test", []string{"NOPE"})
+ assert.Equal(t, expected, actual, "Config did not store values")
+}
diff --git a/config/defaults.go b/config/defaults.go
new file mode 100644
index 0000000..26626ba
--- /dev/null
+++ b/config/defaults.go
@@ -0,0 +1,38 @@
+package config
+
+import (
+ "bytes"
+ "strings"
+ "text/template"
+
+ "github.com/rs/zerolog/log"
+)
+
+var q = `
+INSERT INTO config VALUES('nick','{{.Nick}}');
+INSERT INTO config VALUES('channels','{{.Channel}}');
+INSERT INTO config VALUES('untappd.channels','{{.Channel}}');
+INSERT INTO config VALUES('twitch.channels','{{.Channel}}');
+INSERT INTO config VALUES('init',1);
+`
+
+func (c *Config) SetDefaults(mainChannel, nick string) {
+ if nick == mainChannel && nick == "" {
+ log.Fatal().Msgf("You must provide a nick and a mainChannel")
+ }
+ t := template.Must(template.New("query").Parse(q))
+ vals := struct {
+ Nick string
+ Channel string
+ ChannelKey string
+ }{
+ nick,
+ mainChannel,
+ strings.ToLower(mainChannel),
+ }
+ var buf bytes.Buffer
+ t.Execute(&buf, vals)
+ c.MustExec(`delete from config;`)
+ c.MustExec(buf.String())
+ log.Info().Msgf("Configuration initialized.")
+}
diff --git a/irc/irc.go b/connectors/irc/irc.go
similarity index 67%
rename from irc/irc.go
rename to connectors/irc/irc.go
index b965749..cead610 100644
--- a/irc/irc.go
+++ b/connectors/irc/irc.go
@@ -5,11 +5,11 @@ package irc
import (
"fmt"
"io"
- "log"
"os"
"strings"
"time"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
@@ -42,9 +42,7 @@ type Irc struct {
config *config.Config
quit chan bool
- eventReceived func(msg.Message)
- messageReceived func(msg.Message)
- replyMessageReceived func(msg.Message, string)
+ event bot.Callback
}
func New(c *config.Config) *Irc {
@@ -54,24 +52,28 @@ func New(c *config.Config) *Irc {
return &i
}
-func (i *Irc) RegisterEventReceived(f func(msg.Message)) {
- i.eventReceived = f
+func (i *Irc) RegisterEvent(f bot.Callback) {
+ i.event = f
}
-func (i *Irc) RegisterMessageReceived(f func(msg.Message)) {
- i.messageReceived = f
-}
-
-func (i *Irc) RegisterReplyMessageReceived(f func(msg.Message, string)) {
- i.replyMessageReceived = f
+func (i *Irc) Send(kind bot.Kind, args ...interface{}) (string, error) {
+ switch kind {
+ case bot.Reply:
+ case bot.Message:
+ return i.sendMessage(args[0].(string), args[1].(string), args...)
+ case bot.Action:
+ return i.sendAction(args[0].(string), args[1].(string), args...)
+ default:
+ }
+ return "", nil
}
func (i *Irc) JoinChannel(channel string) {
- log.Printf("Joining channel: %s", channel)
+ log.Info().Msgf("Joining channel: %s", channel)
i.Client.Out <- irc.Msg{Cmd: irc.JOIN, Args: []string{channel}}
}
-func (i *Irc) SendMessage(channel, message string) string {
+func (i *Irc) sendMessage(channel, message string, args ...interface{}) (string, error) {
for len(message) > 0 {
m := irc.Msg{
Cmd: "PRIVMSG",
@@ -87,66 +89,64 @@ func (i *Irc) SendMessage(channel, message string) string {
}
if throttle == nil {
- ratePerSec := i.config.RatePerSec
+ ratePerSec := i.config.GetInt("RatePerSec", 5)
throttle = time.Tick(time.Second / time.Duration(ratePerSec))
}
<-throttle
i.Client.Out <- m
+
+ if len(args) > 0 {
+ for _, a := range args {
+ switch a := a.(type) {
+ case bot.ImageAttachment:
+ m = irc.Msg{
+ Cmd: "PRIVMSG",
+ Args: []string{channel, fmt.Sprintf("%s: %s",
+ a.AltTxt, a.URL)},
+ }
+
+ <-throttle
+
+ i.Client.Out <- m
+ }
+ }
+ }
}
- return "NO_IRC_IDENTIFIERS"
+ return "NO_IRC_IDENTIFIERS", nil
}
// Sends action to channel
-func (i *Irc) SendAction(channel, message string) string {
+func (i *Irc) sendAction(channel, message string, args ...interface{}) (string, error) {
message = actionPrefix + " " + message + "\x01"
- i.SendMessage(channel, message)
- return "NO_IRC_IDENTIFIERS"
-}
-
-func (i *Irc) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
- return "NO_IRC_IDENTIFIERS", false
-}
-
-func (i *Irc) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
- return "NO_IRC_IDENTIFIERS", false
-}
-
-func (i *Irc) React(channel, reaction string, message msg.Message) bool {
- //we're not goign to do anything because it's IRC
- return false
-}
-
-func (i *Irc) Edit(channel, newMessage, identifier string) bool {
- //we're not goign to do anything because it's IRC
- return false
+ return i.sendMessage(channel, message, args...)
}
func (i *Irc) GetEmojiList() map[string]string {
- //we're not goign to do anything because it's IRC
+ //we're not going to do anything because it's IRC
return make(map[string]string)
}
func (i *Irc) Serve() error {
- if i.eventReceived == nil || i.messageReceived == nil {
+ if i.event == nil {
return fmt.Errorf("Missing an event handler")
}
var err error
i.Client, err = irc.DialSSL(
- i.config.Irc.Server,
- i.config.Nick,
- i.config.FullName,
- i.config.Irc.Pass,
+ i.config.Get("Irc.Server", "localhost"),
+ i.config.Get("Nick", "bot"),
+ i.config.Get("FullName", "bot"),
+ i.config.Get("Irc.Pass", ""),
true,
)
if err != nil {
return fmt.Errorf("%s", err)
}
- for _, c := range i.config.Channels {
+ for _, c := range i.config.GetArray("channels", []string{}) {
i.JoinChannel(c)
}
@@ -164,7 +164,7 @@ func (i *Irc) handleConnection() {
close(i.Client.Out)
for err := range i.Client.Errors {
if err != io.EOF {
- log.Println(err)
+ log.Error().Err(err)
}
}
}()
@@ -186,7 +186,7 @@ func (i *Irc) handleConnection() {
case err, ok := <-i.Client.Errors:
if ok && err != io.EOF {
- log.Println(err)
+ log.Error().Err(err)
i.quit <- true
return
}
@@ -200,7 +200,7 @@ func (i *Irc) handleMsg(msg irc.Msg) {
switch msg.Cmd {
case irc.ERROR:
- log.Println(1, "Received error: "+msg.Raw)
+ log.Info().Msgf("Received error: " + msg.Raw)
case irc.PING:
i.Client.Out <- irc.Msg{Cmd: irc.PONG}
@@ -209,56 +209,56 @@ func (i *Irc) handleMsg(msg irc.Msg) {
// OK, ignore
case irc.ERR_NOSUCHNICK:
- i.eventReceived(botMsg)
+ fallthrough
case irc.ERR_NOSUCHCHANNEL:
- i.eventReceived(botMsg)
+ fallthrough
case irc.RPL_MOTD:
- i.eventReceived(botMsg)
+ fallthrough
case irc.RPL_NAMREPLY:
- i.eventReceived(botMsg)
+ fallthrough
case irc.RPL_TOPIC:
- i.eventReceived(botMsg)
+ fallthrough
case irc.KICK:
- i.eventReceived(botMsg)
+ fallthrough
case irc.TOPIC:
- i.eventReceived(botMsg)
+ fallthrough
case irc.MODE:
- i.eventReceived(botMsg)
+ fallthrough
case irc.JOIN:
- i.eventReceived(botMsg)
+ fallthrough
case irc.PART:
- i.eventReceived(botMsg)
+ fallthrough
+
+ case irc.NOTICE:
+ fallthrough
+
+ case irc.NICK:
+ fallthrough
+
+ case irc.RPL_WHOREPLY:
+ fallthrough
+
+ case irc.RPL_ENDOFWHO:
+ i.event(i, bot.Event, botMsg)
+
+ case irc.PRIVMSG:
+ i.event(i, bot.Message, botMsg)
case irc.QUIT:
os.Exit(1)
- case irc.NOTICE:
- i.eventReceived(botMsg)
-
- case irc.PRIVMSG:
- i.messageReceived(botMsg)
-
- case irc.NICK:
- i.eventReceived(botMsg)
-
- case irc.RPL_WHOREPLY:
- i.eventReceived(botMsg)
-
- case irc.RPL_ENDOFWHO:
- i.eventReceived(botMsg)
-
default:
cmd := irc.CmdNames[msg.Cmd]
- log.Println("(" + cmd + ") " + msg.Raw)
+ log.Debug().Msgf("(%s) %s", cmd, msg.Raw)
}
}
@@ -270,7 +270,7 @@ func (i *Irc) buildMessage(inMsg irc.Msg) msg.Message {
}
channel := inMsg.Args[0]
- if channel == i.config.Nick {
+ if channel == i.config.Get("Nick", "bot") {
channel = inMsg.Args[0]
}
diff --git a/slack/fix_text.go b/connectors/slack/fix_text.go
similarity index 100%
rename from slack/fix_text.go
rename to connectors/slack/fix_text.go
diff --git a/slack/slack.go b/connectors/slack/slack.go
similarity index 67%
rename from slack/slack.go
rename to connectors/slack/slack.go
index 32faaaa..bb1ad3d 100644
--- a/slack/slack.go
+++ b/connectors/slack/slack.go
@@ -10,7 +10,6 @@ import (
"html"
"io"
"io/ioutil"
- "log"
"net/http"
"net/url"
"regexp"
@@ -21,6 +20,7 @@ import (
"context"
"time"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
@@ -31,9 +31,10 @@ import (
type Slack struct {
config *config.Config
- url string
- id string
- ws *websocket.Conn
+ url string
+ id string
+ token string
+ ws *websocket.Conn
lastRecieved time.Time
@@ -43,9 +44,7 @@ type Slack struct {
emoji map[string]string
- eventReceived func(msg.Message)
- messageReceived func(msg.Message)
- replyMessageReceived func(msg.Message, string)
+ event bot.Callback
}
var idCounter uint64
@@ -163,15 +162,44 @@ type rtmStart struct {
}
func New(c *config.Config) *Slack {
+ token := c.Get("slack.token", "NONE")
+ if token == "NONE" {
+ log.Fatal().Msgf("No slack token found. Set SLACKTOKEN env.")
+ }
return &Slack{
config: c,
+ token: c.Get("slack.token", ""),
lastRecieved: time.Now(),
users: make(map[string]string),
emoji: make(map[string]string),
}
}
-func checkReturnStatus(response *http.Response) bool {
+func (s *Slack) Send(kind bot.Kind, args ...interface{}) (string, error) {
+ switch kind {
+ case bot.Message:
+ return s.sendMessage(args[0].(string), args[1].(string))
+ case bot.Action:
+ return s.sendAction(args[0].(string), args[1].(string))
+ case bot.Edit:
+ return s.edit(args[0].(string), args[1].(string), args[2].(string))
+ case bot.Reply:
+ switch args[2].(type) {
+ case msg.Message:
+ return s.replyToMessage(args[0].(string), args[1].(string), args[2].(msg.Message))
+ case string:
+ return s.replyToMessageIdentifier(args[0].(string), args[1].(string), args[2].(string))
+ default:
+ return "", fmt.Errorf("Invalid types given to Reply")
+ }
+ case bot.Reaction:
+ return s.react(args[0].(string), args[1].(string), args[2].(msg.Message))
+ default:
+ }
+ return "", fmt.Errorf("No handler for message type %d", kind)
+}
+
+func checkReturnStatus(response *http.Response) error {
type Response struct {
OK bool `json:"ok"`
}
@@ -179,42 +207,34 @@ func checkReturnStatus(response *http.Response) bool {
body, err := ioutil.ReadAll(response.Body)
response.Body.Close()
if err != nil {
- log.Printf("Error reading Slack API body: %s", err)
- return false
+ err := fmt.Errorf("Error reading Slack API body: %s", err)
+ return err
}
var resp Response
err = json.Unmarshal(body, &resp)
if err != nil {
- log.Printf("Error parsing message response: %s", err)
- return false
+ err := fmt.Errorf("Error parsing message response: %s", err)
+ return err
}
- return resp.OK
+ return nil
}
-func (s *Slack) RegisterEventReceived(f func(msg.Message)) {
- s.eventReceived = f
+func (s *Slack) RegisterEvent(f bot.Callback) {
+ s.event = f
}
-func (s *Slack) RegisterMessageReceived(f func(msg.Message)) {
- s.messageReceived = f
-}
-
-func (s *Slack) RegisterReplyMessageReceived(f func(msg.Message, string)) {
- s.replyMessageReceived = f
-}
-
-func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string, error) {
+func (s *Slack) sendMessageType(channel, message string, meMessage bool) (string, error) {
postUrl := "https://slack.com/api/chat.postMessage"
if meMessage {
postUrl = "https://slack.com/api/chat.meMessage"
}
- nick := s.config.Nick
- icon := s.config.IconURL
+ nick := s.config.Get("Nick", "bot")
+ icon := s.config.Get("IconURL", "https://placekitten.com/128/128")
resp, err := http.PostForm(postUrl,
- url.Values{"token": {s.config.Slack.Token},
+ url.Values{"token": {s.token},
"username": {nick},
"icon_url": {icon},
"channel": {channel},
@@ -222,16 +242,16 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
})
if err != nil {
- log.Printf("Error sending Slack message: %s", err)
+ log.Error().Err(err).Msgf("Error sending Slack message")
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
- log.Fatalf("Error reading Slack API body: %s", err)
+ log.Fatal().Err(err).Msgf("Error reading Slack API body")
}
- log.Println(string(body))
+ log.Debug().Msgf("%+v", body)
type MessageResponse struct {
OK bool `json:"ok"`
@@ -244,7 +264,7 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
var mr MessageResponse
err = json.Unmarshal(body, &mr)
if err != nil {
- log.Fatalf("Error parsing message response: %s", err)
+ log.Fatal().Err(err).Msgf("Error parsing message response")
}
if !mr.OK {
@@ -256,24 +276,24 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
return mr.Timestamp, err
}
-func (s *Slack) SendMessage(channel, message string) string {
- log.Printf("Sending message to %s: %s", channel, message)
- identifier, _ := s.SendMessageType(channel, message, false)
- return identifier
+func (s *Slack) sendMessage(channel, message string) (string, error) {
+ log.Debug().Msgf("Sending message to %s: %s", channel, message)
+ identifier, err := s.sendMessageType(channel, message, false)
+ return identifier, err
}
-func (s *Slack) SendAction(channel, message string) string {
- log.Printf("Sending action to %s: %s", channel, message)
- identifier, _ := s.SendMessageType(channel, "_"+message+"_", true)
- return identifier
+func (s *Slack) sendAction(channel, message string) (string, error) {
+ log.Debug().Msgf("Sending action to %s: %s", channel, message)
+ identifier, err := s.sendMessageType(channel, "_"+message+"_", true)
+ return identifier, err
}
-func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
- nick := s.config.Nick
- icon := s.config.IconURL
+func (s *Slack) replyToMessageIdentifier(channel, message, identifier string) (string, error) {
+ nick := s.config.Get("Nick", "bot")
+ icon := s.config.Get("IconURL", "https://placekitten.com/128/128")
resp, err := http.PostForm("https://slack.com/api/chat.postMessage",
- url.Values{"token": {s.config.Slack.Token},
+ url.Values{"token": {s.token},
"username": {nick},
"icon_url": {icon},
"channel": {channel},
@@ -282,18 +302,18 @@ func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (s
})
if err != nil {
- log.Printf("Error sending Slack reply: %s", err)
- return "", false
+ err := fmt.Errorf("Error sending Slack reply: %s", err)
+ return "", err
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
- log.Printf("Error reading Slack API body: %s", err)
- return "", false
+ err := fmt.Errorf("Error reading Slack API body: %s", err)
+ return "", err
}
- log.Println(string(body))
+ log.Debug().Msgf("%s", body)
type MessageResponse struct {
OK bool `json:"ok"`
@@ -303,47 +323,47 @@ func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (s
var mr MessageResponse
err = json.Unmarshal(body, &mr)
if err != nil {
- log.Printf("Error parsing message response: %s", err)
- return "", false
+ err := fmt.Errorf("Error parsing message response: %s", err)
+ return "", err
}
if !mr.OK {
- return "", false
+ return "", fmt.Errorf("Got !OK from slack message response")
}
- return mr.Timestamp, err == nil
+ return mr.Timestamp, err
}
-func (s *Slack) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
- return s.ReplyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"])
+func (s *Slack) replyToMessage(channel, message string, replyTo msg.Message) (string, error) {
+ return s.replyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"])
}
-func (s *Slack) React(channel, reaction string, message msg.Message) bool {
- log.Printf("Reacting in %s: %s", channel, reaction)
+func (s *Slack) react(channel, reaction string, message msg.Message) (string, error) {
+ log.Debug().Msgf("Reacting in %s: %s", channel, reaction)
resp, err := http.PostForm("https://slack.com/api/reactions.add",
- url.Values{"token": {s.config.Slack.Token},
+ url.Values{"token": {s.token},
"name": {reaction},
"channel": {channel},
"timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}})
if err != nil {
- log.Printf("reaction failed: %s", err)
- return false
+ err := fmt.Errorf("reaction failed: %s", err)
+ return "", err
}
- return checkReturnStatus(resp)
+ return "", checkReturnStatus(resp)
}
-func (s *Slack) Edit(channel, newMessage, identifier string) bool {
- log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage)
+func (s *Slack) edit(channel, newMessage, identifier string) (string, error) {
+ log.Debug().Msgf("Editing in (%s) %s: %s", identifier, channel, newMessage)
resp, err := http.PostForm("https://slack.com/api/chat.update",
- url.Values{"token": {s.config.Slack.Token},
+ url.Values{"token": {s.token},
"channel": {channel},
"text": {newMessage},
"ts": {identifier}})
if err != nil {
- log.Printf("edit failed: %s", err)
- return false
+ err := fmt.Errorf("edit failed: %s", err)
+ return "", err
}
- return checkReturnStatus(resp)
+ return "", checkReturnStatus(resp)
}
func (s *Slack) GetEmojiList() map[string]string {
@@ -352,16 +372,16 @@ func (s *Slack) GetEmojiList() map[string]string {
func (s *Slack) populateEmojiList() {
resp, err := http.PostForm("https://slack.com/api/emoji.list",
- url.Values{"token": {s.config.Slack.Token}})
+ url.Values{"token": {s.token}})
if err != nil {
- log.Printf("Error retrieving emoji list from Slack: %s", err)
+ log.Debug().Msgf("Error retrieving emoji list from Slack: %s", err)
return
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
- log.Fatalf("Error reading Slack API body: %s", err)
+ log.Fatal().Err(err).Msgf("Error reading Slack API body")
}
type EmojiListResponse struct {
@@ -372,7 +392,7 @@ func (s *Slack) populateEmojiList() {
var list EmojiListResponse
err = json.Unmarshal(body, &list)
if err != nil {
- log.Fatalf("Error parsing emoji list: %s", err)
+ log.Fatal().Err(err).Msgf("Error parsing emoji list")
}
s.emoji = list.Emoji
}
@@ -397,7 +417,7 @@ func (s *Slack) receiveMessage() (slackMessage, error) {
m := slackMessage{}
err := s.ws.Recv(context.TODO(), &m)
if err != nil {
- log.Println("Error decoding WS message")
+ log.Error().Msgf("Error decoding WS message")
panic(fmt.Errorf("%v\n%v", m, err))
}
return m, nil
@@ -422,7 +442,7 @@ func (s *Slack) Serve() error {
for {
msg, err := s.receiveMessage()
if err != nil && err == io.EOF {
- log.Fatalf("Slack API EOF")
+ log.Fatal().Msg("Slack API EOF")
} else if err != nil {
return fmt.Errorf("Slack API error: %s", err)
}
@@ -432,19 +452,19 @@ func (s *Slack) Serve() error {
if !isItMe && !msg.Hidden && msg.ThreadTs == "" {
m := s.buildMessage(msg)
if m.Time.Before(s.lastRecieved) {
- log.Printf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time)
+ log.Debug().Msgf("Ignoring message: %+v\nlastRecieved: %v msg: %v", msg.ID, s.lastRecieved, m.Time)
} else {
s.lastRecieved = m.Time
- s.messageReceived(m)
+ s.event(s, bot.Message, m)
}
} else if msg.ThreadTs != "" {
//we're throwing away some information here by not parsing the correct reply object type, but that's okay
- s.replyMessageReceived(s.buildLightReplyMessage(msg), msg.ThreadTs)
+ s.event(s, bot.Reply, s.buildLightReplyMessage(msg), msg.ThreadTs)
} else {
- log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID)
+ log.Debug().Msgf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID)
}
case "error":
- log.Printf("Slack error, code: %d, message: %s", msg.Error.Code, msg.Error.Msg)
+ log.Error().Msgf("Slack error, code: %d, message: %s", msg.Error.Code, msg.Error.Msg)
case "": // what even is this?
case "hello":
case "presence_change":
@@ -455,7 +475,7 @@ func (s *Slack) Serve() error {
// squeltch this stuff
continue
default:
- log.Printf("Unhandled Slack message type: '%s'", msg.Type)
+ log.Debug().Msgf("Unhandled Slack message type: '%s'", msg.Type)
}
}
}
@@ -534,25 +554,24 @@ func (s *Slack) buildLightReplyMessage(m slackMessage) msg.Message {
// markAllChannelsRead gets a list of all channels and marks each as read
func (s *Slack) markAllChannelsRead() {
chs := s.getAllChannels()
- log.Printf("Got list of channels to mark read: %+v", chs)
+ log.Debug().Msgf("Got list of channels to mark read: %+v", chs)
for _, ch := range chs {
s.markChannelAsRead(ch.ID)
}
- log.Printf("Finished marking channels read")
+ log.Debug().Msgf("Finished marking channels read")
}
// getAllChannels returns info for all channels joined
func (s *Slack) getAllChannels() []slackChannelListItem {
u := s.url + "channels.list"
resp, err := http.PostForm(u,
- url.Values{"token": {s.config.Slack.Token}})
+ url.Values{"token": {s.token}})
if err != nil {
- log.Printf("Error posting user info request: %s",
- err)
+ log.Error().Err(err).Msgf("Error posting user info request")
return nil
}
if resp.StatusCode != 200 {
- log.Printf("Error posting user info request: %d",
+ log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode)
return nil
}
@@ -560,7 +579,7 @@ func (s *Slack) getAllChannels() []slackChannelListItem {
var chanInfo slackChannelListResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo)
if err != nil || !chanInfo.Ok {
- log.Println("Error decoding response: ", err)
+ log.Error().Err(err).Msgf("Error decoding response")
return nil
}
return chanInfo.Channels
@@ -570,66 +589,62 @@ func (s *Slack) getAllChannels() []slackChannelListItem {
func (s *Slack) markChannelAsRead(slackChanId string) error {
u := s.url + "channels.info"
resp, err := http.PostForm(u,
- url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}})
+ url.Values{"token": {s.token}, "channel": {slackChanId}})
if err != nil {
- log.Printf("Error posting user info request: %s",
- err)
+ log.Error().Err(err).Msgf("Error posting user info request")
return err
}
if resp.StatusCode != 200 {
- log.Printf("Error posting user info request: %d",
+ log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode)
return err
}
defer resp.Body.Close()
var chanInfo slackChannelInfoResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo)
- log.Printf("%+v, %+v", err, chanInfo)
if err != nil || !chanInfo.Ok {
- log.Println("Error decoding response: ", err)
+ log.Error().Err(err).Msgf("Error decoding response")
return err
}
u = s.url + "channels.mark"
resp, err = http.PostForm(u,
- url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}, "ts": {chanInfo.Channel.Latest.Ts}})
+ url.Values{"token": {s.token}, "channel": {slackChanId}, "ts": {chanInfo.Channel.Latest.Ts}})
if err != nil {
- log.Printf("Error posting user info request: %s",
- err)
+ log.Error().Err(err).Msgf("Error posting user info request")
return err
}
if resp.StatusCode != 200 {
- log.Printf("Error posting user info request: %d",
+ log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode)
return err
}
defer resp.Body.Close()
var markInfo map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&markInfo)
- log.Printf("%+v, %+v", err, markInfo)
if err != nil {
- log.Println("Error decoding response: ", err)
+ log.Error().Err(err).Msgf("Error decoding response")
return err
}
- log.Printf("Marked %s as read", slackChanId)
+ log.Info().Msgf("Marked %s as read", slackChanId)
return nil
}
func (s *Slack) connect() {
- token := s.config.Slack.Token
+ token := s.token
u := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token)
resp, err := http.Get(u)
if err != nil {
return
}
if resp.StatusCode != 200 {
- log.Fatalf("Slack API failed. Code: %d", resp.StatusCode)
+ log.Fatal().Msgf("Slack API failed. Code: %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
- log.Fatalf("Error reading Slack API body: %s", err)
+ log.Fatal().Err(err).Msg("Error reading Slack API body")
}
var rtm rtmStart
err = json.Unmarshal(body, &rtm)
@@ -638,7 +653,7 @@ func (s *Slack) connect() {
}
if !rtm.Ok {
- log.Fatalf("Slack error: %s", rtm.Error)
+ log.Fatal().Msgf("Slack error: %s", rtm.Error)
}
s.url = "https://slack.com/api/"
@@ -650,7 +665,7 @@ func (s *Slack) connect() {
rtmURL, _ := url.Parse(rtm.URL)
s.ws, err = websocket.Dial(context.TODO(), rtmURL)
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
}
@@ -660,20 +675,20 @@ func (s *Slack) getUser(id string) (string, bool) {
return name, true
}
- log.Printf("User %s not already found, requesting info", id)
+ log.Debug().Msgf("User %s not already found, requesting info", id)
u := s.url + "users.info"
resp, err := http.PostForm(u,
- url.Values{"token": {s.config.Slack.Token}, "user": {id}})
+ url.Values{"token": {s.token}, "user": {id}})
if err != nil || resp.StatusCode != 200 {
- log.Printf("Error posting user info request: %d %s",
- resp.StatusCode, err)
+ log.Error().Err(err).Msgf("Error posting user info request: %d",
+ resp.StatusCode)
return "UNKNOWN", false
}
defer resp.Body.Close()
var userInfo slackUserInfoResp
err = json.NewDecoder(resp.Body).Decode(&userInfo)
if err != nil {
- log.Println("Error decoding response: ", err)
+ log.Error().Err(err).Msgf("Error decoding response")
return "UNKNOWN", false
}
s.users[id] = userInfo.User.Name
@@ -682,17 +697,18 @@ func (s *Slack) getUser(id string) (string, bool) {
// Who gets usernames out of a channel
func (s *Slack) Who(id string) []string {
- log.Println("Who is queried for ", id)
+ log.Debug().
+ Str("id", id).
+ Msg("Who is queried for ")
u := s.url + "channels.info"
resp, err := http.PostForm(u,
- url.Values{"token": {s.config.Slack.Token}, "channel": {id}})
+ url.Values{"token": {s.token}, "channel": {id}})
if err != nil {
- log.Printf("Error posting user info request: %s",
- err)
+ log.Error().Err(err).Msgf("Error posting user info request")
return []string{}
}
if resp.StatusCode != 200 {
- log.Printf("Error posting user info request: %d",
+ log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode)
return []string{}
}
@@ -700,17 +716,17 @@ func (s *Slack) Who(id string) []string {
var chanInfo slackChannelInfoResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo)
if err != nil || !chanInfo.Ok {
- log.Println("Error decoding response: ", err)
+ log.Error().Err(err).Msgf("Error decoding response")
return []string{}
}
- log.Printf("%#v", chanInfo.Channel)
+ log.Debug().Msgf("%#v", chanInfo.Channel)
handles := []string{}
for _, member := range chanInfo.Channel.Members {
u, _ := s.getUser(member)
handles = append(handles, u)
}
- log.Printf("Returning %d handles", len(handles))
+ log.Debug().Msgf("Returning %d handles", len(handles))
return handles
}
diff --git a/connectors/slackapp/fix_text.go b/connectors/slackapp/fix_text.go
new file mode 100644
index 0000000..a4a7c2f
--- /dev/null
+++ b/connectors/slackapp/fix_text.go
@@ -0,0 +1,97 @@
+package slackapp
+
+import (
+ "github.com/nlopes/slack"
+ "unicode/utf8"
+)
+
+// fixText strips all of the Slack-specific annotations from message text,
+// replacing it with the equivalent display form.
+// Currently it:
+// • Replaces user mentions like <@U124356> with @ followed by the user's nick.
+// This uses the lookupUser function, which must map U1243456 to the nick.
+// • Replaces user mentions like with the user's nick.
+// • Strips < and > surrounding links.
+//
+// This was directly bogarted from velour/chat with emoji conversion removed.
+func fixText(findUser func(id string) (*slack.User, error), text string) string {
+ var output []rune
+ for len(text) > 0 {
+ r, i := utf8.DecodeRuneInString(text)
+ text = text[i:]
+ switch {
+ case r == '<':
+ var tag []rune
+ for {
+ r, i := utf8.DecodeRuneInString(text)
+ text = text[i:]
+ switch {
+ case r == '>':
+ if t, ok := fixTag(findUser, tag); ok {
+ output = append(output, t...)
+ break
+ }
+ fallthrough
+ case len(text) == 0:
+ output = append(output, '<')
+ output = append(output, tag...)
+ output = append(output, r)
+ default:
+ tag = append(tag, r)
+ continue
+ }
+ break
+ }
+ default:
+ output = append(output, r)
+ }
+ }
+ return string(output)
+}
+
+func fixTag(findUser func(string) (*slack.User, error), tag []rune) ([]rune, bool) {
+ switch {
+ case hasPrefix(tag, "@U"):
+ if i := indexRune(tag, '|'); i >= 0 {
+ return tag[i+1:], true
+ }
+ if findUser != nil {
+ if u, err := findUser(string(tag[1:])); err == nil {
+ return []rune(u.Name), true
+ }
+ }
+ return tag, true
+
+ case hasPrefix(tag, "#C"):
+ if i := indexRune(tag, '|'); i >= 0 {
+ return append([]rune{'#'}, tag[i+1:]...), true
+ }
+
+ case hasPrefix(tag, "http"):
+ if i := indexRune(tag, '|'); i >= 0 {
+ tag = tag[:i]
+ }
+ return tag, true
+ }
+
+ return nil, false
+}
+
+func hasPrefix(text []rune, prefix string) bool {
+ for _, r := range prefix {
+ if len(text) == 0 || text[0] != r {
+ return false
+ }
+ text = text[1:]
+ }
+ return true
+}
+
+func indexRune(text []rune, find rune) int {
+ for i, r := range text {
+ if r == find {
+ return i
+ }
+ }
+ return -1
+}
diff --git a/connectors/slackapp/slackApp.go b/connectors/slackapp/slackApp.go
new file mode 100644
index 0000000..07b9a8d
--- /dev/null
+++ b/connectors/slackapp/slackApp.go
@@ -0,0 +1,569 @@
+package slackapp
+
+import (
+ "bytes"
+ "container/ring"
+ "encoding/json"
+ "fmt"
+ "html"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+ "text/template"
+ "time"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/nlopes/slack"
+ "github.com/nlopes/slack/slackevents"
+
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+ "github.com/velour/catbase/config"
+)
+
+const DefaultRing = 5
+const defaultLogFormat = "[{{fixDate .Time \"2006-01-02 15:04:05\"}}] {{if .TopicChange}}*** {{.User.Name}}{{else if .Action}}* {{.User.Name}}{{else}}<{{.User.Name}}>{{end}} {{.Body}}\n"
+
+// 11:10AM DBG connectors/slackapp/slackApp.go:496 > Slack event dir=logs raw={"Action":false,"AdditionalData":
+// {"RAW_SLACK_TIMESTAMP":"1559920235.001100"},"Body":"aoeu","Channel":"C0S04SMRC","ChannelName":"test",
+// "Command":false,"Host":"","IsIM":false,"Raw":{"channel":"C0S04SMRC","channel_type":"channel",
+// "event_ts":1559920235.001100,"files":null,"text":"aoeu","thread_ts":"","ts":"1559920235.001100",
+// "type":"message","upload":false,"user":"U0RLUDELD"},"Time":"2019-06-07T11:10:35.0000011-04:00",
+// "User":{"Admin":false,"ID":"U0RLUDELD","Name":"flyngpngn"}}
+
+type SlackApp struct {
+ bot bot.Bot
+ config *config.Config
+ api *slack.Client
+
+ botToken string
+ userToken string
+ verification string
+ id string
+
+ lastRecieved time.Time
+
+ myBotID string
+ users map[string]*slack.User
+ emoji map[string]string
+ channels map[string]*slack.Channel
+
+ event bot.Callback
+
+ msgIDBuffer *ring.Ring
+
+ logFormat *template.Template
+}
+
+func fixDate(input time.Time, format string) string {
+ return input.Format(format)
+}
+func New(c *config.Config) *SlackApp {
+ token := c.Get("slack.token", "NONE")
+ if token == "NONE" {
+ log.Fatal().Msg("No slack token found. Set SLACKTOKEN env.")
+ }
+
+ api := slack.New(token, slack.OptionDebug(false))
+
+ idBuf := ring.New(c.GetInt("ringSize", DefaultRing))
+ for i := 0; i < idBuf.Len(); i++ {
+ idBuf.Value = ""
+ idBuf = idBuf.Next()
+ }
+
+ tplTxt := c.GetString("slackapp.log.format", defaultLogFormat)
+ funcs := template.FuncMap{
+ "fixDate": fixDate,
+ }
+ tpl := template.Must(template.New("log").Funcs(funcs).Parse(tplTxt))
+
+ return &SlackApp{
+ api: api,
+ config: c,
+ botToken: token,
+ userToken: c.Get("slack.usertoken", "NONE"),
+ verification: c.Get("slack.verification", "NONE"),
+ myBotID: c.Get("slack.botid", ""),
+ lastRecieved: time.Now(),
+ users: make(map[string]*slack.User),
+ emoji: make(map[string]string),
+ channels: make(map[string]*slack.Channel),
+ msgIDBuffer: idBuf,
+ logFormat: tpl,
+ }
+}
+
+func (s *SlackApp) RegisterEvent(f bot.Callback) {
+ s.event = f
+}
+
+func (s *SlackApp) Serve() error {
+ s.populateEmojiList()
+
+ http.HandleFunc("/evt", func(w http.ResponseWriter, r *http.Request) {
+ buf := new(bytes.Buffer)
+ buf.ReadFrom(r.Body)
+ body := buf.String()
+ eventsAPIEvent, e := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionVerifyToken(&slackevents.TokenComparator{VerificationToken: s.verification}))
+ if e != nil {
+ log.Error().Err(e)
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+
+ if eventsAPIEvent.Type == slackevents.URLVerification {
+ var r *slackevents.ChallengeResponse
+ err := json.Unmarshal([]byte(body), &r)
+ if err != nil {
+ log.Error().Err(err)
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ w.Header().Set("Content-Type", "text")
+ w.Write([]byte(r.Challenge))
+ } else if eventsAPIEvent.Type == slackevents.CallbackEvent {
+ innerEvent := eventsAPIEvent.InnerEvent
+ switch ev := innerEvent.Data.(type) {
+ case *slackevents.AppMentionEvent:
+ // This is a bit of a problem. AppMentionEvent also needs to
+ // End up in msgReceived
+ //s.msgReceivd(ev)
+ case *slackevents.MessageEvent:
+ s.msgReceivd(ev)
+ }
+ } else {
+ log.Debug().
+ Str("type", eventsAPIEvent.Type).
+ Interface("event", eventsAPIEvent).
+ Msg("event")
+ }
+ })
+ return nil
+}
+
+// checkRingOrAdd returns true if it finds the ts value
+// or false if the ts isn't yet in the ring (and adds it)
+func (s *SlackApp) checkRingOrAdd(ts string) bool {
+ found := false
+ s.msgIDBuffer.Do(func(p interface{}) {
+ if p.(string) == ts {
+ found = true
+ }
+ })
+ if found {
+ return true
+ }
+ s.msgIDBuffer.Value = ts
+ s.msgIDBuffer = s.msgIDBuffer.Next()
+ return false
+}
+
+func (s *SlackApp) msgReceivd(msg *slackevents.MessageEvent) {
+ if msg.TimeStamp == "" {
+ log.Debug().
+ Str("type", msg.SubType).
+ Msg("ignoring an unhandled event type")
+ return
+ }
+
+ log.Debug().
+ Interface("event", msg).
+ Str("type", msg.SubType).
+ Msg("accepting a message")
+
+ if s.checkRingOrAdd(msg.TimeStamp) {
+ log.Debug().
+ Str("ts", msg.TimeStamp).
+ Msg("Got a duplicate message from server")
+ return
+ }
+
+ isItMe := msg.BotID != "" && msg.BotID == s.myBotID
+ m := s.buildMessage(msg)
+ if m.Time.Before(s.lastRecieved) {
+ log.Debug().
+ Time("ts", m.Time).
+ Interface("lastRecv", s.lastRecieved).
+ Msg("Ignoring message")
+ return
+ }
+ if err := s.log(m); err != nil {
+ log.Fatal().Err(err).Msg("Error logging message")
+ }
+ if !isItMe && msg.ThreadTimeStamp == "" {
+ s.lastRecieved = m.Time
+ s.event(s, bot.Message, m)
+ } else if msg.ThreadTimeStamp != "" {
+ //we're throwing away some information here by not parsing the correct reply object type, but that's okay
+ s.event(s, bot.Reply, s.buildMessage(msg), msg.ThreadTimeStamp)
+ } else if isItMe {
+ s.event(s, bot.SelfMessage, m)
+ } else {
+ log.Debug().
+ Str("text", msg.Text).
+ Msg("Unknown message is hidden")
+ }
+}
+
+func (s *SlackApp) Send(kind bot.Kind, args ...interface{}) (string, error) {
+ switch kind {
+ case bot.Message:
+ return s.sendMessage(args[0].(string), args[1].(string), false, args...)
+ case bot.Action:
+ return s.sendMessage(args[0].(string), args[1].(string), true, args...)
+ case bot.Edit:
+ return s.edit(args[0].(string), args[1].(string), args[2].(string))
+ case bot.Reply:
+ switch args[2].(type) {
+ case msg.Message:
+ return s.replyToMessage(args[0].(string), args[1].(string), args[2].(msg.Message))
+ case string:
+ return s.replyToMessageIdentifier(args[0].(string), args[1].(string), args[2].(string))
+ default:
+ return "", fmt.Errorf("Invalid types given to Reply")
+ }
+ case bot.Reaction:
+ return s.react(args[0].(string), args[1].(string), args[2].(msg.Message))
+ default:
+ }
+ return "", fmt.Errorf("No handler for message type %d", kind)
+}
+
+func (s *SlackApp) sendMessage(channel, message string, meMessage bool, args ...interface{}) (string, error) {
+ ts, err := "", fmt.Errorf("")
+ nick := s.config.Get("Nick", "bot")
+
+ options := []slack.MsgOption{
+ slack.MsgOptionUsername(nick),
+ slack.MsgOptionText(message, false),
+ }
+ if meMessage {
+ options = append(options, slack.MsgOptionMeMessage())
+ }
+
+ // Check for message attachments
+ attachments := []slack.Attachment{}
+ if len(args) > 0 {
+ for _, a := range args {
+ switch a := a.(type) {
+ case bot.ImageAttachment:
+ attachments = append(attachments, slack.Attachment{
+ ImageURL: a.URL,
+ Text: a.AltTxt,
+ })
+ }
+ }
+ }
+
+ if len(attachments) > 0 {
+ options = append(options, slack.MsgOptionAttachments(attachments...))
+ }
+
+ log.Debug().
+ Str("channel", channel).
+ Str("message", message).
+ Int("attachment count", len(attachments)).
+ Int("option count", len(options)).
+ Int("arg count", len(args)).
+ Msg("Sending message")
+
+ _, ts, err = s.api.PostMessage(channel, options...)
+
+ if err != nil {
+ log.Error().Err(err).Msg("Error sending message")
+ return "", err
+ }
+
+ return ts, nil
+}
+
+func (s *SlackApp) replyToMessageIdentifier(channel, message, identifier string) (string, error) {
+ nick := s.config.Get("Nick", "bot")
+ icon := s.config.Get("IconURL", "https://placekitten.com/128/128")
+
+ resp, err := http.PostForm("https://slack.com/api/chat.postMessage",
+ url.Values{"token": {s.botToken},
+ "username": {nick},
+ "icon_url": {icon},
+ "channel": {channel},
+ "text": {message},
+ "thread_ts": {identifier},
+ })
+
+ if err != nil {
+ err := fmt.Errorf("Error sending Slack reply: %s", err)
+ return "", err
+ }
+
+ body, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ err := fmt.Errorf("Error reading Slack API body: %s", err)
+ return "", err
+ }
+
+ log.Debug().Bytes("body", body)
+
+ type MessageResponse struct {
+ OK bool `json:"ok"`
+ Timestamp string `json:"ts"`
+ }
+
+ var mr MessageResponse
+ err = json.Unmarshal(body, &mr)
+ if err != nil {
+ err := fmt.Errorf("Error parsing message response: %s", err)
+ return "", err
+ }
+
+ if !mr.OK {
+ return "", fmt.Errorf("Got !OK from slack message response")
+ }
+
+ return mr.Timestamp, err
+}
+
+func (s *SlackApp) replyToMessage(channel, message string, replyTo msg.Message) (string, error) {
+ return s.replyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"])
+}
+
+func (s *SlackApp) react(channel, reaction string, message msg.Message) (string, error) {
+ log.Debug().
+ Str("channel", channel).
+ Str("reaction", reaction).
+ Msg("reacting")
+ ref := slack.ItemRef{
+ Channel: channel,
+ Timestamp: message.AdditionalData["RAW_SLACK_TIMESTAMP"],
+ }
+ err := s.api.AddReaction(reaction, ref)
+ return "", err
+}
+
+func (s *SlackApp) edit(channel, newMessage, identifier string) (string, error) {
+ log.Debug().
+ Str("channel", channel).
+ Str("identifier", identifier).
+ Str("newMessage", newMessage).
+ Msg("editing")
+ nick := s.config.Get("Nick", "bot")
+ _, ts, err := s.api.PostMessage(channel,
+ slack.MsgOptionUsername(nick),
+ slack.MsgOptionText(newMessage, false),
+ slack.MsgOptionMeMessage(),
+ slack.MsgOptionUpdate(identifier))
+ return ts, err
+}
+
+func (s *SlackApp) GetEmojiList() map[string]string {
+ return s.emoji
+}
+
+func (s *SlackApp) populateEmojiList() {
+ if s.userToken == "NONE" {
+ log.Error().Msg("Cannot get emoji list without slack.usertoken")
+ return
+ }
+ api := slack.New(s.userToken, slack.OptionDebug(false))
+
+ em, err := api.GetEmoji()
+ if err != nil {
+ log.Error().Err(err).Msg("Error retrieving emoji list from Slack")
+ return
+ }
+
+ s.emoji = em
+}
+
+// I think it's horseshit that I have to do this
+func slackTStoTime(t string) time.Time {
+ ts := strings.Split(t, ".")
+ if len(ts) < 2 {
+ log.Fatal().
+ Str("ts", t).
+ Msg("Could not parse Slack timestamp")
+ }
+ sec, _ := strconv.ParseInt(ts[0], 10, 64)
+ nsec, _ := strconv.ParseInt(ts[1], 10, 64)
+ return time.Unix(sec, nsec)
+}
+
+var urlDetector = regexp.MustCompile(`<(.+)://([^|^>]+).*>`)
+
+// Convert a slackMessage to a msg.Message
+func (s *SlackApp) buildMessage(m *slackevents.MessageEvent) msg.Message {
+ text := html.UnescapeString(m.Text)
+
+ text = fixText(s.getUser, text)
+
+ isCmd, text := bot.IsCmd(s.config, text)
+
+ isAction := m.SubType == "me_message"
+
+ // We have to try a few layers to get a valid name for the user because Slack
+ name := "UNKNOWN"
+ u, _ := s.getUser(m.User)
+ if u != nil {
+ name = u.Profile.DisplayName
+ }
+ if m.Username != "" && u == nil {
+ name = m.Username
+ }
+
+ chName := m.Channel
+ if ch, _ := s.getChannel(m.Channel); ch != nil {
+ chName = ch.Name
+ }
+
+ tstamp := slackTStoTime(m.TimeStamp)
+
+ return msg.Message{
+ User: &user.User{
+ ID: m.User,
+ Name: name,
+ },
+ Body: text,
+ Raw: m,
+ Channel: m.Channel,
+ ChannelName: chName,
+ IsIM: m.ChannelType == "im",
+ Command: isCmd,
+ Action: isAction,
+ Time: tstamp,
+ AdditionalData: map[string]string{
+ "RAW_SLACK_TIMESTAMP": m.TimeStamp,
+ },
+ }
+}
+
+func (s *SlackApp) getChannel(id string) (*slack.Channel, error) {
+ if ch, ok := s.channels[id]; ok {
+ return ch, nil
+ }
+
+ log.Debug().
+ Str("id", id).
+ Msg("Channel not known, requesting info")
+
+ ch, err := s.api.GetChannelInfo(id)
+ if err != nil {
+ return nil, err
+ }
+ s.channels[id] = ch
+ return s.channels[id], nil
+}
+
+// Get username for Slack user ID
+func (s *SlackApp) getUser(id string) (*slack.User, error) {
+ if name, ok := s.users[id]; ok {
+ return name, nil
+ }
+
+ log.Debug().
+ Str("id", id).
+ Msg("User not already found, requesting info")
+ u, err := s.api.GetUserInfo(id)
+ if err != nil {
+ return nil, err
+ }
+ s.users[id] = u
+ return s.users[id], nil
+}
+
+// Who gets usernames out of a channel
+func (s *SlackApp) Who(id string) []string {
+ if s.userToken == "NONE" {
+ log.Error().Msg("Cannot get emoji list without slack.usertoken")
+ return []string{s.config.Get("nick", "bot")}
+ }
+ api := slack.New(s.userToken, slack.OptionDebug(false))
+
+ log.Debug().
+ Str("id", id).
+ Msg("Who is queried")
+ // Not super sure this is the correct call
+ params := &slack.GetUsersInConversationParameters{
+ ChannelID: id,
+ Limit: 50,
+ }
+ members, _, err := api.GetUsersInConversation(params)
+ if err != nil {
+ log.Error().Err(err)
+ return []string{s.config.Get("nick", "bot")}
+ }
+
+ ret := []string{}
+ for _, m := range members {
+ u, err := s.getUser(m)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Str("user", m).
+ Msg("Couldn't get user")
+ continue
+ /**/
+ }
+ ret = append(ret, u.Name)
+ }
+ return ret
+}
+
+// log writes to a /.log
+// Uses slackapp.log.format to write entries
+func (s *SlackApp) log(raw msg.Message) error {
+
+ // Do some filtering and fixing up front
+ if raw.Body == "" {
+ return nil
+ }
+
+ data := struct {
+ msg.Message
+ TopicChange bool
+ }{
+ Message: raw,
+ }
+
+ if strings.Contains(raw.Body, "set the channel topic: ") {
+ topic := strings.SplitN(raw.Body, "set the channel topic: ", 2)
+ data.Body = "changed topic to " + topic[1]
+ data.TopicChange = true
+ }
+
+ dir := path.Join(s.config.Get("slackapp.log.dir", "logs"), raw.ChannelName)
+ now := time.Now()
+ fname := now.Format("20060102") + ".log"
+ path := path.Join(dir, fname)
+
+ log.Debug().
+ Interface("raw", raw).
+ Str("dir", dir).
+ Msg("Slack event")
+
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ log.Error().
+ Err(err).
+ Msg("Could not create log directory")
+ return err
+ }
+
+ f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ log.Fatal().
+ Err(err).
+ Msg("Error opening log file")
+ }
+ defer f.Close()
+
+ if err := s.logFormat.Execute(f, data); err != nil {
+ return err
+ }
+
+ return f.Sync()
+}
diff --git a/connectors/slackapp/slackApp_test.go b/connectors/slackapp/slackApp_test.go
new file mode 100644
index 0000000..939f1fa
--- /dev/null
+++ b/connectors/slackapp/slackApp_test.go
@@ -0,0 +1,58 @@
+package slackapp
+
+import (
+ "container/ring"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestDedupeNoDupes(t *testing.T) {
+ buf := ring.New(3)
+ for i := 0; i < 3; i++ {
+ buf.Value = ""
+ buf = buf.Next()
+ }
+ s := SlackApp{msgIDBuffer: buf}
+ expected := []bool{
+ false,
+ false,
+ false,
+ false,
+ false,
+ }
+
+ actuals := []bool{}
+ actuals = append(actuals, s.checkRingOrAdd("a"))
+ actuals = append(actuals, s.checkRingOrAdd("b"))
+ actuals = append(actuals, s.checkRingOrAdd("c"))
+ actuals = append(actuals, s.checkRingOrAdd("d"))
+ actuals = append(actuals, s.checkRingOrAdd("e"))
+
+ assert.ElementsMatch(t, expected, actuals)
+}
+
+func TestDedupeWithDupes(t *testing.T) {
+ buf := ring.New(3)
+ for i := 0; i < 3; i++ {
+ buf.Value = ""
+ buf = buf.Next()
+ }
+ s := SlackApp{msgIDBuffer: buf}
+ expected := []bool{
+ false,
+ false,
+ true,
+ false,
+ true,
+ }
+
+ actuals := []bool{}
+ actuals = append(actuals, s.checkRingOrAdd("a"))
+ actuals = append(actuals, s.checkRingOrAdd("b"))
+ actuals = append(actuals, s.checkRingOrAdd("a"))
+ actuals = append(actuals, s.checkRingOrAdd("d"))
+ actuals = append(actuals, s.checkRingOrAdd("d"))
+
+ assert.ElementsMatch(t, expected, actuals)
+}
diff --git a/example_config.lua b/example_config.lua
deleted file mode 100644
index b1a2ea3..0000000
--- a/example_config.lua
+++ /dev/null
@@ -1,123 +0,0 @@
-config = {
- Channels = {
- "#CatBaseTest"
- },
- TwitterConsumerSecret = "",
- Reminder = {
- MaxBatchAdd = 10
- },
- Nick = "CatBaseTest",
- IconURL = "http://placekitten.com/g/200/300",
- LeftPad = {
- Who = "person",
- MaxLen = 50
- },
- Factoid = {
- StartupFact = "speed test",
- QuoteTime = 1,
- QuoteChance = 0.99,
- MinLen = 5
- },
- CommandChar = {
- "!",
- "¡"
- },
- FullName = "CatBase",
- Your = {
- MaxLength = 140,
- DuckingChance = 0.5,
- FuckingChance = 0.15,
- YourChance = 0.4
- },
- Emojify = {
- Chance = 0.02,
- Scoreless = {
- "a",
- "it"
- }
- },
- DB = {
- File = "catbase.db",
- Server = "127.0.0.1"
- },
- Plugins = {
- },
- Untappd = {
- Freq = 3600,
- Channels = {
- },
- Token = ""
- },
- LogLength = 50,
- RatePerSec = 10,
- Reaction = {
- HarrassChance = 0.05,
- GeneralChance = 0.01,
- NegativeHarrassmentMultiplier = 2,
- HarrassList = {
- "msherms"
- },
- NegativeReactions = {
- "bullshit",
- "fake",
- "tableflip",
- "vomit"
- },
- PositiveReactions = {
- "+1",
- "authorized",
- "aw_yea",
- "joy"
- }
- },
- TwitterUserKey = "",
- MainChannel = "#CatBaseTest",
- TwitterUserSecret = "",
- WelcomeMsgs = {
- "Real men use screen, %s.",
- "Joins upset the hivemind's OCD, %s.",
- "Joins upset the hivemind's CDO, %s.",
- "%s, I WILL CUT YOU!"
- },
- Bad = {
- Msgs = {
- },
- Hosts = {
- },
- Nicks = {
- }
- },
- Irc = {
- Server = "ircserver:6697",
- Pass = "CatBaseTest:test"
- },
- Slack = {
- Token = ""
- },
- TwitterConsumerKey = "",
- Babbler = {
- DefaultUsers = {
- "seabass"
- }
- },
- Type = "slack",
- Admins = {
- ""
- },
- Stats = {
- Sightings = {
- "user"
- },
- DBPath = "stats.db"
- },
- HttpAddr = "127.0.0.1:1337",
- Inventory = {
- Max = 5
- },
- Sisyphus = {
- MinDecrement = 10,
- MinPush = 1
- }
-}
-
-}
diff --git a/go.mod b/go.mod
index 313d566..3cb56db 100644
--- a/go.mod
+++ b/go.mod
@@ -1,25 +1,46 @@
module github.com/velour/catbase
require (
- github.com/PuerkitoBio/goquery v1.5.0 // indirect
- github.com/boltdb/bolt v1.3.1
+ github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c
+ github.com/PuerkitoBio/goquery v1.5.0
+ github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd // indirect
+ github.com/armon/go-radix v1.0.0 // indirect
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
- github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect
+ github.com/fogleman/gg v1.3.0 // indirect
+ github.com/go-sql-driver/mysql v1.4.1 // indirect
+ github.com/golang/protobuf v1.3.2 // indirect
+ github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
+ github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
+ github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889
+ github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 // indirect
github.com/jmoiron/sqlx v1.2.0
- github.com/mattn/go-sqlite3 v1.10.0
- github.com/mitchellh/mapstructure v1.1.2 // indirect
+ github.com/jung-kurt/gofpdf v1.7.0 // indirect
+ github.com/lib/pq v1.2.0 // indirect
+ github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
+ github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect
+ github.com/mattn/go-sqlite3 v1.11.0
github.com/mmcdole/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/nlopes/slack v0.5.0
+ github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254
+ github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
- github.com/stretchr/objx v0.1.1 // indirect
- github.com/stretchr/testify v1.2.2
+ github.com/rs/zerolog v1.15.0
+ github.com/spaolacci/murmur3 v1.1.0 // indirect
+ github.com/stretchr/objx v0.2.0 // indirect
+ github.com/stretchr/testify v1.3.0
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
- github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7
- github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec
- golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect
- golang.org/x/text v0.3.0 // indirect
+ golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
+ golang.org/x/mobile v0.0.0-20190806162312-597adff16ade // indirect
+ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
+ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
+ golang.org/x/tools v0.0.0-20190813142322-97f12d73768f // indirect
+ gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b // indirect
+ gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e // indirect
+ gonum.org/v1/plot v0.0.0-20190615073203-9aa86143727f // indirect
+ google.golang.org/appengine v1.6.1 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)
diff --git a/go.sum b/go.sum
index 187d147..fe11ac3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,50 +1,172 @@
+github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE=
+github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c h1:bJ0HbTMaInVjakxM76G+2gsmbKTdHzpTUGyLGYxdMO0=
+github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c/go.mod h1:8+24kIp7vJsYy0GmQDDNnPwAYEWkl3OcaPxJSDAfe1U=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
+github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
+github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
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/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
+github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
+github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18=
+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/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/james-bowman/nlp v0.0.0-20190301165020-c5645f996605 h1:MjLvsJmW4uoTjleqqiL0wKRTjxUakKUhDNoSsSlS2hk=
+github.com/james-bowman/nlp v0.0.0-20190301165020-c5645f996605/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
+github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889 h1:VYwE/yKDYXpd5hno5fCCWv2wPcM37DYAX4r3Re1pLz8=
+github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
+github.com/james-bowman/sparse v0.0.0-20190309194602-7d83420cfcbe h1:UFAsFuH6cu/0Lx+qBWfxiO69jrPkvdbG3qwSWI/7yF0=
+github.com/james-bowman/sparse v0.0.0-20190309194602-7d83420cfcbe/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
+github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 h1:ph/BDQQDL41apnHSN48I5GyNOQXXAlc79HwGqDSXCss=
+github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
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/jung-kurt/gofpdf v1.0.0/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/jung-kurt/gofpdf v1.7.0/go.mod h1:s/VXv+TdctEOx2wCEguezYaR7f0OwUAd6H9VGfRkcSs=
+github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
+github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
+github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
+github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
+github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
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/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/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
+github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
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/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
+github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
+github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
+github.com/olebedev/when v0.0.0-20190131080308-164b69386514 h1:xpZutaUgtGPKT2JFaH72/yby908QS9ORlnrAkkdJ4m0=
+github.com/olebedev/when v0.0.0-20190131080308-164b69386514/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
+github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 h1:JYoQR67E1vv1WGoeW8DkdFs7vrIEe/5wP+qJItd5tUE=
+github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
+github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
+github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/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/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
+github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+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/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
+github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
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/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/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/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/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/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+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-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 h1:Ep4L2ibjtJcW6IP73KbcJAU0cpNKsLNSSP2jE1xlCys=
+golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190806162312-597adff16ade/go.mod h1:AlhUtkH4DA4asiFC5RgK7ZKmauvtkAVcy9L0epCzlWo=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/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-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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0=
+golang.org/x/net v0.0.0-20190606173856-1492cefac77f/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-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190813142322-97f12d73768f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-20190325211145-e42c1265cdd5 h1:tyvqqvbB9Sn6UPjokEzsK6cCE9k4Tx/AHGGaJiLIk7g=
+gonum.org/v1/gonum v0.0.0-20190325211145-e42c1265cdd5/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
+gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
+gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b h1:wlZ2AJblZitrh7dfm5OX2WenXLBZCuWqUeNczop2lPA=
+gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
+gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+gonum.org/v1/plot v0.0.0-20190615073203-9aa86143727f/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
+google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
+modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/main.go b/main.go
index 3b1a22f..9e9dcb8 100644
--- a/main.go
+++ b/main.go
@@ -4,19 +4,26 @@ package main
import (
"flag"
- "log"
+ "github.com/velour/catbase/plugins/cli"
+ "github.com/velour/catbase/plugins/newsbid"
"math/rand"
+ "net/http"
+ "os"
"time"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/config"
- "github.com/velour/catbase/irc"
+ "github.com/velour/catbase/connectors/irc"
+ "github.com/velour/catbase/connectors/slack"
+ "github.com/velour/catbase/connectors/slackapp"
"github.com/velour/catbase/plugins/admin"
"github.com/velour/catbase/plugins/babbler"
"github.com/velour/catbase/plugins/beers"
"github.com/velour/catbase/plugins/couldashouldawoulda"
"github.com/velour/catbase/plugins/counter"
- "github.com/velour/catbase/plugins/db"
"github.com/velour/catbase/plugins/dice"
"github.com/velour/catbase/plugins/emojifyme"
"github.com/velour/catbase/plugins/fact"
@@ -26,68 +33,107 @@ import (
"github.com/velour/catbase/plugins/nerdepedia"
"github.com/velour/catbase/plugins/picker"
"github.com/velour/catbase/plugins/reaction"
+ "github.com/velour/catbase/plugins/remember"
"github.com/velour/catbase/plugins/reminder"
"github.com/velour/catbase/plugins/rpgORdie"
"github.com/velour/catbase/plugins/rss"
"github.com/velour/catbase/plugins/sisyphus"
+ "github.com/velour/catbase/plugins/stock"
"github.com/velour/catbase/plugins/talker"
"github.com/velour/catbase/plugins/tell"
+ "github.com/velour/catbase/plugins/tldr"
"github.com/velour/catbase/plugins/twitch"
"github.com/velour/catbase/plugins/your"
"github.com/velour/catbase/plugins/zork"
- "github.com/velour/catbase/slack"
+)
+
+var (
+ key = flag.String("set", "", "Configuration key to set")
+ val = flag.String("val", "", "Configuration value to set")
+ initDB = flag.Bool("init", false, "Initialize the configuration DB")
+ prettyLog = flag.Bool("pretty", false, "Use pretty console logger")
+ debug = flag.Bool("debug", false, "Turn on debug logging")
)
func main() {
rand.Seed(time.Now().Unix())
- var cfile = flag.String("config", "config.lua",
- "Config file to load. (Defaults to config.lua)")
+ var dbpath = flag.String("db", "catbase.db",
+ "Database file to load. (Defaults to catbase.db)")
flag.Parse() // parses the logging flags.
- c := config.Readconfig(Version, *cfile)
+ log.Logger = log.With().Caller().Stack().Logger()
+ if *prettyLog {
+ log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+ }
+ zerolog.SetGlobalLevel(zerolog.InfoLevel)
+ if *debug {
+ zerolog.SetGlobalLevel(zerolog.DebugLevel)
+ }
+
+ c := config.ReadConfig(*dbpath)
+
+ if *key != "" && *val != "" {
+ c.Set(*key, *val)
+ log.Info().Msgf("Set config %s: %s", *key, *val)
+ return
+ }
+ if (*initDB && len(flag.Args()) != 2) || (!*initDB && c.GetInt("init", 0) != 1) {
+ log.Fatal().Msgf(`You must run "catbase -init "`)
+ } else if *initDB {
+ c.SetDefaults(flag.Arg(0), flag.Arg(1))
+ return
+ }
+
var client bot.Connector
- switch c.Type {
+ switch c.Get("type", "slackapp") {
case "irc":
client = irc.New(c)
case "slack":
client = slack.New(c)
+ case "slackapp":
+ client = slackapp.New(c)
default:
- log.Fatalf("Unknown connection type: %s", c.Type)
+ log.Fatal().Msgf("Unknown connection type: %s", c.Get("type", "UNSET"))
}
b := bot.New(c, client)
- b.AddHandler("admin", admin.New(b))
- b.AddHandler("first", first.New(b))
- b.AddHandler("leftpad", leftpad.New(b))
- b.AddHandler("talker", talker.New(b))
- b.AddHandler("dice", dice.New(b))
- b.AddHandler("picker", picker.New(b))
- b.AddHandler("beers", beers.New(b))
- b.AddHandler("remember", fact.NewRemember(b))
- b.AddHandler("your", your.New(b))
- b.AddHandler("counter", counter.New(b))
- b.AddHandler("reminder", reminder.New(b))
- b.AddHandler("babbler", babbler.New(b))
- b.AddHandler("zork", zork.New(b))
- b.AddHandler("rss", rss.New(b))
- b.AddHandler("reaction", reaction.New(b))
- b.AddHandler("emojifyme", emojifyme.New(b))
- b.AddHandler("twitch", twitch.New(b))
- b.AddHandler("inventory", inventory.New(b))
- b.AddHandler("rpgORdie", rpgORdie.New(b))
- b.AddHandler("sisyphus", sisyphus.New(b))
- b.AddHandler("tell", tell.New(b))
- b.AddHandler("couldashouldawoulda", couldashouldawoulda.New(b))
- b.AddHandler("nedepedia", nerdepedia.New(b))
+ b.AddPlugin(admin.New(b))
+ b.AddPlugin(emojifyme.New(b))
+ b.AddPlugin(first.New(b))
+ b.AddPlugin(leftpad.New(b))
+ b.AddPlugin(talker.New(b))
+ b.AddPlugin(dice.New(b))
+ b.AddPlugin(picker.New(b))
+ b.AddPlugin(beers.New(b))
+ b.AddPlugin(remember.New(b))
+ b.AddPlugin(your.New(b))
+ b.AddPlugin(counter.New(b))
+ b.AddPlugin(reminder.New(b))
+ b.AddPlugin(babbler.New(b))
+ b.AddPlugin(zork.New(b))
+ b.AddPlugin(rss.New(b))
+ b.AddPlugin(reaction.New(b))
+ b.AddPlugin(twitch.New(b))
+ b.AddPlugin(inventory.New(b))
+ b.AddPlugin(rpgORdie.New(b))
+ b.AddPlugin(sisyphus.New(b))
+ b.AddPlugin(tell.New(b))
+ b.AddPlugin(couldashouldawoulda.New(b))
+ b.AddPlugin(nerdepedia.New(b))
+ b.AddPlugin(tldr.New(b))
+ b.AddPlugin(stock.New(b))
+ b.AddPlugin(newsbid.New(b))
+ b.AddPlugin(cli.New(b))
// catches anything left, will always return true
- b.AddHandler("factoid", fact.New(b))
- b.AddHandler("db", db.New(b))
+ b.AddPlugin(fact.New(b))
- for {
- err := client.Serve()
- log.Println(err)
+ if err := client.Serve(); err != nil {
+ log.Fatal().Err(err)
}
+
+ addr := c.Get("HttpAddr", "127.0.0.1:1337")
+ log.Fatal().Err(http.ListenAndServe(addr, nil))
}
diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go
index ae624b2..094ec76 100644
--- a/plugins/admin/admin.go
+++ b/plugins/admin/admin.go
@@ -3,55 +3,121 @@
package admin
import (
- "log"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
"strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/config"
)
// This is a admin plugin to serve as an example and quick copy/paste for new plugins.
type AdminPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
db *sqlx.DB
+ cfg *config.Config
+
+ quiet bool
}
// NewAdminPlugin creates a new AdminPlugin with the Plugin interface
-func New(bot bot.Bot) *AdminPlugin {
+func New(b bot.Bot) *AdminPlugin {
p := &AdminPlugin{
- Bot: bot,
- db: bot.DB(),
+ bot: b,
+ db: b.DB(),
+ cfg: b.Config(),
}
- p.LoadData()
+ b.Register(p, bot.Message, p.message)
+ b.Register(p, bot.Help, p.help)
+ p.registerWeb()
return p
}
+var forbiddenKeys = map[string]bool{
+ "twitch.authorization": true,
+ "twitch.clientid": true,
+ "untappd.token": true,
+ "slack.token": true,
+}
+
// 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 *AdminPlugin) Message(message msg.Message) bool {
+func (p *AdminPlugin) message(conn bot.Connector, k bot.Kind, message msg.Message, args ...interface{}) bool {
body := message.Body
+ if p.quiet {
+ return true
+ }
+
if len(body) > 0 && body[0] == '$' {
- return p.handleVariables(message)
+ return p.handleVariables(conn, message)
+ }
+
+ if !message.Command {
+ return false
+ }
+
+ if strings.ToLower(body) == "shut up" {
+ dur := time.Duration(p.cfg.GetInt("quietDuration", 5)) * time.Minute
+ log.Info().Msgf("Going to sleep for %v, %v", dur, time.Now().Add(dur))
+ p.bot.Send(conn, bot.Message, message.Channel, "Okay. I'll be back later.")
+ p.quiet = true
+ go func() {
+ select {
+ case <-time.After(dur):
+ p.quiet = false
+ log.Info().Msg("Waking up from nap.")
+ }
+ }()
+ return true
+ }
+
+ if strings.ToLower(body) == "password" {
+ p.bot.Send(conn, bot.Message, message.Channel, p.bot.GetPassword())
+ return true
+ }
+
+ parts := strings.Split(body, " ")
+ if parts[0] == "set" && len(parts) > 2 && forbiddenKeys[parts[1]] {
+ p.bot.Send(conn, bot.Message, message.Channel, "You cannot access that key")
+ return true
+ } else if parts[0] == "set" && len(parts) > 2 {
+ p.cfg.Set(parts[1], strings.Join(parts[2:], " "))
+ p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("Set %s", parts[1]))
+ return true
+ }
+ if parts[0] == "get" && len(parts) == 2 && forbiddenKeys[parts[1]] {
+ p.bot.Send(conn, bot.Message, message.Channel, "You cannot access that key")
+ return true
+ } else if parts[0] == "get" && len(parts) == 2 {
+ v := p.cfg.Get(parts[1], "")
+ p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("%s: %s", parts[1], v))
+ return true
}
return false
}
-func (p *AdminPlugin) handleVariables(message msg.Message) bool {
+func (p *AdminPlugin) handleVariables(conn bot.Connector, message msg.Message) bool {
if parts := strings.SplitN(message.Body, "!=", 2); len(parts) == 2 {
variable := strings.ToLower(strings.TrimSpace(parts[0]))
value := strings.TrimSpace(parts[1])
_, err := p.db.Exec(`delete from variables where name=? and value=?`, variable, value)
if err != nil {
- p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
- log.Println("[admin]: ", err)
+ p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
+ log.Error().Err(err)
} else {
- p.Bot.SendMessage(message.Channel, "Removed.")
+ p.bot.Send(conn, bot.Message, message.Channel, "Removed.")
}
return true
@@ -69,50 +135,65 @@ func (p *AdminPlugin) handleVariables(message msg.Message) bool {
row := p.db.QueryRow(`select count(*) from variables where value = ?`, variable, value)
err := row.Scan(&count)
if err != nil {
- p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
- log.Println("[admin]: ", err)
+ p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
+ log.Error().Err(err)
return true
}
if count > 0 {
- p.Bot.SendMessage(message.Channel, "I've already got that one.")
+ p.bot.Send(conn, bot.Message, message.Channel, "I've already got that one.")
} else {
_, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value)
if err != nil {
- p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
- log.Println("[admin]: ", err)
+ p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
+ log.Error().Err(err)
return true
}
- p.Bot.SendMessage(message.Channel, "Added.")
+ p.bot.Send(conn, bot.Message, message.Channel, "Added.")
}
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 *AdminPlugin) LoadData() {
- // This bot has no data to load
-}
-
// Help responds to help requests. Every plugin must implement a help function.
-func (p *AdminPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "This does super secret things that you're not allowed to know about.")
+func (p *AdminPlugin) help(conn bot.Connector, kind bot.Kind, m msg.Message, args ...interface{}) bool {
+ p.bot.Send(conn, bot.Message, m.Channel, "This does super secret things that you're not allowed to know about.")
+ return true
}
-// Empty event handler because this plugin does not do anything on event recv
-func (p *AdminPlugin) Event(kind string, message msg.Message) bool {
- return false
+func (p *AdminPlugin) registerWeb() {
+ http.HandleFunc("/vars/api", p.handleWebAPI)
+ http.HandleFunc("/vars", p.handleWeb)
+ p.bot.RegisterWeb("/vars", "Variables")
}
-// Handler for bot's own messages
-func (p *AdminPlugin) BotMessage(message msg.Message) bool {
- return false
+var tpl = template.Must(template.New("factoidIndex").Parse(varIndex))
+
+func (p *AdminPlugin) handleWeb(w http.ResponseWriter, r *http.Request) {
+ tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.bot.GetWebNavigation()})
}
-// Register any web URLs desired
-func (p *AdminPlugin) RegisterWeb() *string {
- return nil
+func (p *AdminPlugin) handleWebAPI(w http.ResponseWriter, r *http.Request) {
+ var configEntries []struct {
+ Key string `json:"key"`
+ Value string `json:"value"`
+ }
+ q := `select key, value from config`
+ err := p.db.Select(&configEntries, q)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Msg("Error getting config entries.")
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ for i, e := range configEntries {
+ if strings.Contains(e.Value, ";;") {
+ e.Value = strings.ReplaceAll(e.Value, ";;", ", ")
+ e.Value = fmt.Sprintf("[%s]", e.Value)
+ configEntries[i] = e
+ }
+ }
+ j, _ := json.Marshal(configEntries)
+ fmt.Fprintf(w, "%s", j)
}
-
-func (p *AdminPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/admin/admin_test.go b/plugins/admin/admin_test.go
new file mode 100644
index 0000000..917b5b9
--- /dev/null
+++ b/plugins/admin/admin_test.go
@@ -0,0 +1,71 @@
+package admin
+
+import (
+ "github.com/velour/catbase/plugins/cli"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+)
+
+var (
+ a *AdminPlugin
+ mb *bot.MockBot
+)
+
+func setup(t *testing.T) (*AdminPlugin, *bot.MockBot) {
+ mb = bot.NewMockBot()
+ a = New(mb)
+ mb.DB().MustExec(`delete from config`)
+ return a, mb
+}
+
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
+ isCmd := strings.HasPrefix(payload, "!")
+ if isCmd {
+ payload = payload[1:]
+ }
+ c := cli.CliPlugin{}
+ return &c, bot.Message, msg.Message{
+ User: &user.User{Name: "tester"},
+ Channel: "test",
+ Body: payload,
+ Command: isCmd,
+ }
+}
+
+func TestSet(t *testing.T) {
+ a, mb := setup(t)
+ expected := "test value"
+ a.message(makeMessage("!set test.key " + expected))
+ actual := mb.Config().Get("test.key", "ERR")
+ assert.Equal(t, expected, actual)
+}
+
+func TestGetValue(t *testing.T) {
+ a, mb := setup(t)
+ expected := "value"
+ mb.Config().Set("test.key", "value")
+ a.message(makeMessage("!get test.key"))
+ assert.Len(t, mb.Messages, 1)
+ assert.Contains(t, mb.Messages[0], expected)
+}
+
+func TestGetEmpty(t *testing.T) {
+ a, mb := setup(t)
+ expected := "test.key: "
+ a.message(makeMessage("!get test.key"))
+ assert.Len(t, mb.Messages, 1)
+ assert.Equal(t, expected, mb.Messages[0])
+}
+
+func TestGetForbidden(t *testing.T) {
+ a, mb := setup(t)
+ expected := "cannot access"
+ a.message(makeMessage("!get slack.token"))
+ assert.Len(t, mb.Messages, 1)
+ assert.Contains(t, mb.Messages[0], expected)
+}
diff --git a/plugins/admin/index.go b/plugins/admin/index.go
new file mode 100644
index 0000000..bcf5c68
--- /dev/null
+++ b/plugins/admin/index.go
@@ -0,0 +1,75 @@
+package admin
+
+var varIndex = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Vars
+
+
+
+
+
+ Variables
+
+ {{ "{{ item.Name }}" }}
+
+
+
+ {{ "{{ err }}" }}
+
+
+
+
+
+
+
+
+
+`
diff --git a/plugins/babbler/babbler.go b/plugins/babbler/babbler.go
index 36f1521..2b3854e 100644
--- a/plugins/babbler/babbler.go
+++ b/plugins/babbler/babbler.go
@@ -6,14 +6,14 @@ import (
"database/sql"
"errors"
"fmt"
- "log"
"math/rand"
"strings"
+ "github.com/rs/zerolog/log"
+
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
- "github.com/velour/catbase/config"
)
var (
@@ -25,7 +25,6 @@ var (
type BabblerPlugin struct {
Bot bot.Bot
db *sqlx.DB
- config *config.Config
WithGoRoutines bool
}
@@ -54,55 +53,55 @@ type BabblerArc struct {
Frequency int64 `db:"frequency"`
}
-func New(bot bot.Bot) *BabblerPlugin {
- log.SetFlags(log.LstdFlags | log.Lshortfile)
-
- if _, err := bot.DB().Exec(`create table if not exists babblers (
+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)
+ log.Fatal().Err(err)
}
- if _, err := bot.DB().Exec(`create table if not exists babblerWords (
+ if _, err := b.DB().Exec(`create table if not exists babblerWords (
id integer primary key,
word string
);`); err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
- if _, err := bot.DB().Exec(`create table if not exists babblerNodes (
+ 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)
+ log.Fatal().Err(err)
}
- if _, err := bot.DB().Exec(`create table if not exists babblerArcs (
+ 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)
+ log.Fatal().Err(err)
}
plugin := &BabblerPlugin{
- Bot: bot,
- db: bot.DB(),
- config: bot.Config(),
+ Bot: b,
+ db: b.DB(),
WithGoRoutines: true,
}
plugin.createNewWord("")
+ b.Register(plugin, bot.Message, plugin.message)
+ b.Register(plugin, bot.Help, plugin.help)
+
return plugin
}
-func (p *BabblerPlugin) Message(message msg.Message) bool {
+func (p *BabblerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
lowercase := strings.ToLower(message.Body)
tokens := strings.Fields(lowercase)
numTokens := len(tokens)
@@ -144,12 +143,12 @@ func (p *BabblerPlugin) Message(message msg.Message) bool {
}
if saidSomething {
- p.Bot.SendMessage(message.Channel, saidWhat)
+ p.Bot.Send(c, bot.Message, message.Channel, saidWhat)
}
return saidSomething
}
-func (p *BabblerPlugin) Help(channel string, parts []string) {
+func (p *BabblerPlugin) help(c bot.Connector, kind bot.Kind, msg msg.Message, args ...interface{}) bool {
commands := []string{
"initialize babbler for seabass",
"merge babbler drseabass into seabass",
@@ -158,19 +157,8 @@ func (p *BabblerPlugin) Help(channel string, parts []string) {
"seabass says-middle-out ...",
"seabass says-bridge ... | ...",
}
- p.Bot.SendMessage(channel, strings.Join(commands, "\n\n"))
-}
-
-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
+ p.Bot.Send(c, bot.Message, msg.Channel, strings.Join(commands, "\n\n"))
+ return true
}
func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) {
@@ -178,7 +166,7 @@ func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) {
if err == nil {
id, err := res.LastInsertId()
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
return &Babbler{
@@ -194,11 +182,10 @@ func (p *BabblerPlugin) getBabbler(name string) (*Babbler, error) {
err := p.db.QueryRowx(`select * from babblers where babbler = ? LIMIT 1;`, name).StructScan(&bblr)
if err != nil {
if err == sql.ErrNoRows {
- log.Printf("failed to find babbler")
+ log.Error().Msg("failed to find babbler")
return nil, NO_BABBLER
}
- log.Printf("encountered problem in babbler lookup")
- log.Print(err)
+ log.Error().Err(err).Msg("encountered problem in babbler lookup")
return nil, err
}
return &bblr, nil
@@ -209,13 +196,13 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
if err == NO_BABBLER {
babbler, err = p.makeBabbler(name)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
rows, err := p.db.Queryx(fmt.Sprintf("select tidbit from factoid where fact like '%s quotes';", babbler.Name))
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return babbler, nil
}
defer rows.Close()
@@ -225,10 +212,10 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
var tidbit string
err := rows.Scan(&tidbit)
- log.Print(tidbit)
+ log.Debug().Str("tidbit", tidbit)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return babbler, err
}
tidbits = append(tidbits, tidbit)
@@ -236,7 +223,7 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
for _, tidbit := range tidbits {
if err = p.addToMarkovChain(babbler, tidbit); err != nil {
- log.Print(err)
+ log.Error().Err(err)
}
}
}
@@ -258,12 +245,12 @@ func (p *BabblerPlugin) getWord(word string) (*BabblerWord, error) {
func (p *BabblerPlugin) createNewWord(word string) (*BabblerWord, error) {
res, err := p.db.Exec(`insert into babblerWords (word) values (?);`, word)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
id, err := res.LastInsertId()
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
return &BabblerWord{
@@ -277,7 +264,7 @@ func (p *BabblerPlugin) getOrCreateWord(word string) (*BabblerWord, error) {
return p.createNewWord(word)
} else {
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
}
return w, err
}
@@ -303,19 +290,19 @@ func (p *BabblerPlugin) getBabblerNode(babbler *Babbler, word string) (*BabblerN
func (p *BabblerPlugin) createBabblerNode(babbler *Babbler, word string) (*BabblerNode, error) {
w, err := p.getOrCreateWord(word)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
res, err := p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?, ?, 0, 0)`, babbler.BabblerId, w.WordId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
id, err := res.LastInsertId()
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
@@ -338,12 +325,12 @@ func (p *BabblerPlugin) getOrCreateBabblerNode(babbler *Babbler, word string) (*
func (p *BabblerPlugin) incrementRootWordFrequency(babbler *Babbler, word string) (*BabblerNode, error) {
node, err := p.getOrCreateBabblerNode(babbler, word)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
_, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + 1, root = 1 where id = ?;`, node.NodeId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
node.RootFrequency += 1
@@ -365,7 +352,7 @@ func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerAr
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)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
@@ -377,7 +364,7 @@ func (p *BabblerPlugin) incrementWordArc(fromNode, toNode *BabblerNode) (*Babble
if affectedRows == 0 {
res, err = p.db.Exec(`insert into babblerArcs (fromNodeId, toNodeId, frequency) values (?, ?, 1);`, fromNode.NodeId, toNode.NodeId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
}
@@ -401,19 +388,19 @@ func (p *BabblerPlugin) addToMarkovChain(babbler *Babbler, phrase string) error
curNode, err := p.incrementRootWordFrequency(babbler, words[0])
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
for i := 1; i < len(words); i++ {
nextNode, err := p.getOrCreateBabblerNode(babbler, words[i])
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
_, err = p.incrementWordArc(curNode, nextNode)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
curNode = nextNode
@@ -426,7 +413,7 @@ func (p *BabblerPlugin) addToMarkovChain(babbler *Babbler, phrase string) 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)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
defer rows.Close()
@@ -438,7 +425,7 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba
var node BabblerNode
err = rows.StructScan(&node)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
rootNodes = append(rootNodes, &node)
@@ -457,21 +444,21 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba
var w BabblerWord
err := p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
return node, &w, nil
}
}
- log.Fatalf("shouldn't happen")
- return nil, nil, errors.New("failed to find weighted root word")
+ log.Fatal().Msg("failed to find weighted root word")
+ return nil, nil, nil
}
func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode, *BabblerWord, error) {
rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, fromNode.NodeId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
defer rows.Close()
@@ -482,7 +469,7 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
arcs = append(arcs, &arc)
@@ -503,28 +490,28 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode
var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.ToNodeId).StructScan(&node)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
return &node, &w, nil
}
}
- log.Fatalf("shouldn't happen")
- return nil, nil, errors.New("failed to find weighted next word")
+ log.Fatal().Msg("failed to find weighted next word")
+ return nil, nil, nil
}
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.Print(err)
+ log.Error().Err(err)
return nil, nil, false, err
}
defer rows.Close()
@@ -535,7 +522,7 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, false, err
}
arcs = append(arcs, &arc)
@@ -562,39 +549,39 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo
var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.FromNodeId).StructScan(&node)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, false, err
}
var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, false, err
}
return &node, &w, false, nil
}
}
- log.Fatalf("shouldn't happen")
- return nil, nil, false, errors.New("failed to find weighted previous word")
+ log.Fatal().Msg("failed to find weighted previous word")
+ return nil, nil, false, nil
}
func (p *BabblerPlugin) verifyPhrase(babbler *Babbler, phrase []string) (*BabblerNode, *BabblerNode, error) {
curNode, err := p.getBabblerNode(babbler, phrase[0])
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
firstNode := curNode
for i := 1; i < len(phrase); i++ {
nextNode, err := p.getBabblerNode(babbler, phrase[i])
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
_, err = p.getBabblerArc(curNode, nextNode)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, nil, err
}
curNode = nextNode
@@ -610,7 +597,7 @@ func (p *BabblerPlugin) babble(who string) (string, error) {
func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, error) {
babbler, err := p.getBabbler(babblerName)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", nil
}
@@ -621,14 +608,14 @@ func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, e
if len(seed) == 0 {
curNode, curWord, err = p.getWeightedRootNode(babbler)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
words = append(words, curWord.Word)
} else {
_, curNode, err = p.verifyPhrase(babbler, seed)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
}
@@ -636,7 +623,7 @@ func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, e
for {
curNode, curWord, err = p.getWeightedNextWord(curNode)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
if curWord.Word == " " {
@@ -655,12 +642,12 @@ func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, e
func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoName, otherName string) error {
intoNode, err := p.getOrCreateBabblerNode(intoBabbler, "<"+intoName+">")
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
otherNode, err := p.getOrCreateBabblerNode(otherBabbler, "<"+otherName+">")
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
@@ -668,7 +655,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
rows, err := p.db.Queryx("select * from babblerNodes where babblerId = ?;", otherBabbler.BabblerId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
defer rows.Close()
@@ -679,7 +666,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var node BabblerNode
err = rows.StructScan(&node)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
nodes = append(nodes, &node)
@@ -695,12 +682,12 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
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)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
}
} else {
res, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + ? where babblerId = ? and wordId = ?;`, node.RootFrequency, intoBabbler.BabblerId, node.WordId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
}
}
@@ -712,7 +699,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
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 {
- log.Print(err)
+ log.Error().Err(err)
return err
}
}
@@ -720,7 +707,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var updatedNode BabblerNode
err = p.db.QueryRowx(`select * from babblerNodes where babblerId = ? and wordId = ? LIMIT 1;`, intoBabbler.BabblerId, node.WordId).StructScan(&updatedNode)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
@@ -740,7 +727,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return err
}
arcs = append(arcs, &arc)
@@ -760,13 +747,13 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
func (p *BabblerPlugin) babbleSeedSuffix(babblerName string, seed []string) (string, error) {
babbler, err := p.getBabbler(babblerName)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", nil
}
firstNode, curNode, err := p.verifyPhrase(babbler, seed)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
@@ -777,7 +764,7 @@ func (p *BabblerPlugin) babbleSeedSuffix(babblerName string, seed []string) (str
for {
curNode, curWord, shouldTerminate, err = p.getWeightedPreviousWord(curNode)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
@@ -806,7 +793,7 @@ func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error)
arcs := []*BabblerArc{}
rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, babblerNodeId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return arcs, err
}
defer rows.Close()
@@ -815,7 +802,7 @@ func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error)
var arc BabblerArc
err = rows.StructScan(&arc)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return []*BabblerArc{}, err
}
arcs = append(arcs, &arc)
@@ -827,7 +814,7 @@ func (p *BabblerPlugin) getBabblerNodeById(nodeId int64) (*BabblerNode, error) {
var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, nodeId).StructScan(&node)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil, err
}
return &node, nil
@@ -843,19 +830,19 @@ func shuffle(a []*BabblerArc) {
func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []string) (string, error) {
babbler, err := p.getBabbler(babblerName)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", nil
}
_, startWordNode, err := p.verifyPhrase(babbler, start)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
endWordNode, _, err := p.verifyPhrase(babbler, end)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
@@ -864,7 +851,7 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
previous *searchNode
}
- open := []*searchNode{&searchNode{startWordNode.NodeId, nil}}
+ open := []*searchNode{{startWordNode.NodeId, nil}}
closed := map[int64]*searchNode{startWordNode.NodeId: open[0]}
goalNodeId := int64(-1)
@@ -909,13 +896,13 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
for {
cur, err := p.getBabblerNodeById(curSearchNode.babblerNodeId)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, cur.WordId).StructScan(&w)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", err
}
words = append(words, w.Word)
@@ -937,5 +924,3 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
return strings.Join(words, " "), nil
}
-
-func (p *BabblerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/babbler/babbler_test.go b/plugins/babbler/babbler_test.go
index d598359..0239396 100644
--- a/plugins/babbler/babbler_test.go
+++ b/plugins/babbler/babbler_test.go
@@ -3,6 +3,7 @@
package babbler
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,13 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
+ c := &cli.CliPlugin{}
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return c, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -28,15 +30,20 @@ func makeMessage(payload string) msg.Message {
func newBabblerPlugin(mb *bot.MockBot) *BabblerPlugin {
bp := New(mb)
bp.WithGoRoutines = false
+ mb.DB().MustExec(`
+ delete from babblers;
+ delete from babblerWords;
+ delete from babblerNodes;
+ delete from babblerArcs;
+ `)
return bp
}
func TestBabblerNoBabbler(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- bp.Message(makeMessage("!seabass2 says"))
+ bp.message(makeMessage("!seabass2 says"))
res := assert.Len(t, mb.Messages, 0)
assert.True(t, res)
// assert.Contains(t, mb.Messages[0], "seabass2 babbler not found")
@@ -45,11 +52,10 @@ func TestBabblerNoBabbler(t *testing.T) {
func TestBabblerNothingSaid(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- res := bp.Message(makeMessage("initialize babbler for seabass"))
+ res := bp.message(makeMessage("initialize babbler for seabass"))
assert.True(t, res)
- res = bp.Message(makeMessage("!seabass says"))
+ res = bp.message(makeMessage("!seabass says"))
assert.True(t, res)
assert.Len(t, mb.Messages, 2)
assert.Contains(t, mb.Messages[0], "okay.")
@@ -59,16 +65,15 @@ func TestBabblerNothingSaid(t *testing.T) {
func TestBabbler(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "This is another message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is")
@@ -78,16 +83,15 @@ func TestBabbler(t *testing.T) {
func TestBabblerSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "This is another message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says long"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says long"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "long message")
@@ -96,16 +100,15 @@ func TestBabblerSeed(t *testing.T) {
func TestBabblerMultiSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "This is another message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says This is a long"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says This is a long"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is a long message")
@@ -114,16 +117,15 @@ func TestBabblerMultiSeed(t *testing.T) {
func TestBabblerMultiSeed2(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "This is another message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says is a long"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says is a long"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "is a long message")
@@ -132,16 +134,15 @@ func TestBabblerMultiSeed2(t *testing.T) {
func TestBabblerBadSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- bp.Message(seabass)
+ bp.message(c, k, seabass)
seabass.Body = "This is another message"
- bp.Message(seabass)
+ bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- bp.Message(seabass)
- bp.Message(makeMessage("!seabass says noooo this is bad"))
+ bp.message(c, k, seabass)
+ bp.message(makeMessage("!seabass says noooo this is bad"))
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "seabass never said 'noooo this is bad'")
}
@@ -149,16 +150,15 @@ func TestBabblerBadSeed(t *testing.T) {
func TestBabblerBadSeed2(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is a message")
+ c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"}
- bp.Message(seabass)
+ bp.message(c, k, seabass)
seabass.Body = "This is another message"
- bp.Message(seabass)
+ bp.message(c, k, seabass)
seabass.Body = "This is a long message"
- bp.Message(seabass)
- bp.Message(makeMessage("!seabass says This is a really"))
+ bp.message(c, k, seabass)
+ bp.message(makeMessage("!seabass says This is a really"))
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "seabass never said 'this is a really'")
}
@@ -166,17 +166,16 @@ func TestBabblerBadSeed2(t *testing.T) {
func TestBabblerSuffixSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is message one")
+ c, k, seabass := makeMessage("This is message one")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "It's easier to test with unique messages"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "hi there"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-tail message one"))
- res = bp.Message(makeMessage("!seabass says-tail with unique"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-tail message one"))
+ res = bp.message(makeMessage("!seabass says-tail with unique"))
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is message one")
@@ -186,16 +185,15 @@ func TestBabblerSuffixSeed(t *testing.T) {
func TestBabblerBadSuffixSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("This is message one")
+ c, k, seabass := makeMessage("This is message one")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
seabass.Body = "It's easier to test with unique messages"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = "hi there"
- res = bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-tail anything true"))
+ res = bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-tail anything true"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "seabass never said 'anything true'")
@@ -204,12 +202,11 @@ func TestBabblerBadSuffixSeed(t *testing.T) {
func TestBabblerBookendSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("It's easier to test with unique messages")
+ c, k, seabass := makeMessage("It's easier to test with unique messages")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-bridge It's easier | unique messages"))
+ res := bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-bridge It's easier | unique messages"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages")
@@ -218,12 +215,11 @@ func TestBabblerBookendSeed(t *testing.T) {
func TestBabblerBookendSeedShort(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("It's easier to test with unique messages")
+ c, k, seabass := makeMessage("It's easier to test with unique messages")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-bridge It's easier to test with | unique messages"))
+ res := bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-bridge It's easier to test with | unique messages"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages")
@@ -232,12 +228,11 @@ func TestBabblerBookendSeedShort(t *testing.T) {
func TestBabblerBadBookendSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("It's easier to test with unique messages")
+ c, k, seabass := makeMessage("It's easier to test with unique messages")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-bridge It's easier | not unique messages"))
+ res := bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-bridge It's easier | not unique messages"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "seabass never said 'it's easier ... not unique messages'")
@@ -246,12 +241,11 @@ func TestBabblerBadBookendSeed(t *testing.T) {
func TestBabblerMiddleOutSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("It's easier to test with unique messages")
+ c, k, seabass := makeMessage("It's easier to test with unique messages")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-middle-out test with"))
+ res := bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-middle-out test with"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages")
@@ -260,12 +254,11 @@ func TestBabblerMiddleOutSeed(t *testing.T) {
func TestBabblerBadMiddleOutSeed(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("It's easier to test with unique messages")
+ c, k, seabass := makeMessage("It's easier to test with unique messages")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
- res = bp.Message(makeMessage("!seabass says-middle-out anything true"))
+ res := bp.message(c, k, seabass)
+ res = bp.message(makeMessage("!seabass says-middle-out anything true"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Equal(t, mb.Messages[0], "seabass never said 'anything true'")
@@ -274,12 +267,11 @@ func TestBabblerBadMiddleOutSeed(t *testing.T) {
func TestBabblerBatch(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage("batch learn for seabass This is a message! This is another message. This is not a long message? This is not a message! This is not another message. This is a long message?")
- res := bp.Message(seabass)
+ c, k, seabass := makeMessage("batch learn for seabass This is a message! This is another message. This is not a long message? This is not a message! This is not another message. This is a long message?")
+ res := bp.message(c, k, seabass)
assert.Len(t, mb.Messages, 1)
- res = bp.Message(makeMessage("!seabass says"))
+ res = bp.message(makeMessage("!seabass says"))
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
assert.Contains(t, mb.Messages[1], "this is")
@@ -289,26 +281,25 @@ func TestBabblerBatch(t *testing.T) {
func TestBabblerMerge(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
- bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp)
- seabass := makeMessage(" This is a message")
+ c, k, seabass := makeMessage(" This is a message")
seabass.User = &user.User{Name: "seabass"}
- res := bp.Message(seabass)
+ res := bp.message(c, k, seabass)
assert.Len(t, mb.Messages, 0)
seabass.Body = " This is another message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
seabass.Body = " This is a long message"
- res = bp.Message(seabass)
+ res = bp.message(c, k, seabass)
- res = bp.Message(makeMessage("!merge babbler seabass into seabass2"))
+ res = bp.message(makeMessage("!merge babbler seabass into seabass2"))
assert.True(t, res)
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "mooooiggged")
- res = bp.Message(makeMessage("!seabass2 says"))
+ res = bp.message(makeMessage("!seabass2 says"))
assert.True(t, res)
assert.Len(t, mb.Messages, 2)
@@ -320,27 +311,7 @@ func TestHelp(t *testing.T) {
mb := bot.NewMockBot()
bp := newBabblerPlugin(mb)
assert.NotNil(t, bp)
- bp.Help("channel", []string{})
+ c := &cli.CliPlugin{}
+ bp.help(c, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- mb := bot.NewMockBot()
- bp := newBabblerPlugin(mb)
- assert.NotNil(t, bp)
- assert.False(t, bp.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- mb := bot.NewMockBot()
- bp := newBabblerPlugin(mb)
- assert.NotNil(t, bp)
- assert.False(t, bp.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- mb := bot.NewMockBot()
- bp := newBabblerPlugin(mb)
- assert.NotNil(t, bp)
- assert.Nil(t, bp.RegisterWeb())
-}
diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go
index cd019e2..8b818fb 100644
--- a/plugins/beers/beers.go
+++ b/plugins/beers/beers.go
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io/ioutil"
- "log"
"math/rand"
"net/http"
"strconv"
@@ -15,6 +14,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/plugins/counter"
@@ -37,34 +37,33 @@ type untappdUser struct {
chanNick string
}
-// NewBeersPlugin creates a new BeersPlugin with the Plugin interface
-func New(bot bot.Bot) *BeersPlugin {
- if bot.DBVersion() == 1 {
- if _, err := bot.DB().Exec(`create table if not exists untappd (
+// New BeersPlugin creates a new BeersPlugin with the Plugin interface
+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)
- }
+ log.Fatal().Err(err)
}
- p := BeersPlugin{
- Bot: bot,
- db: bot.DB(),
+ p := &BeersPlugin{
+ Bot: b,
+ db: b.DB(),
}
- p.LoadData()
- for _, channel := range bot.Config().Untappd.Channels {
- go p.untappdLoop(channel)
+ for _, channel := range b.Config().GetArray("Untappd.Channels", []string{}) {
+ go p.untappdLoop(b.DefaultConnector(), channel)
}
- return &p
+ b.Register(p, bot.Message, p.message)
+ b.Register(p, bot.Help, p.help)
+ 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 *BeersPlugin) Message(message msg.Message) bool {
+func (p *BeersPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
parts := strings.Fields(message.Body)
if len(parts) == 0 {
@@ -84,49 +83,49 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
count, err := strconv.Atoi(parts[2])
if err != nil {
// if it's not a number, maybe it's a nick!
- p.Bot.SendMessage(channel, "Sorry, that didn't make any sense.")
+ p.Bot.Send(c, bot.Message, channel, "Sorry, that didn't make any sense.")
}
if count < 0 {
// you can't be negative
msg := fmt.Sprintf("Sorry %s, you can't have negative beers!", nick)
- p.Bot.SendMessage(channel, msg)
+ p.Bot.Send(c, bot.Message, channel, msg)
return true
}
if parts[1] == "+=" {
p.addBeers(nick, count)
- p.randomReply(channel)
+ p.randomReply(c, channel)
} else if parts[1] == "=" {
if count == 0 {
- p.puke(nick, channel)
+ p.puke(c, nick, channel)
} else {
p.setBeers(nick, count)
- p.randomReply(channel)
+ p.randomReply(c, channel)
}
} else {
- p.Bot.SendMessage(channel, "I don't know your math.")
+ p.Bot.Send(c, bot.Message, channel, "I don't know your math.")
}
} else if len(parts) == 2 {
if p.doIKnow(parts[1]) {
- p.reportCount(parts[1], channel, false)
+ p.reportCount(c, parts[1], channel, false)
} else {
msg := fmt.Sprintf("Sorry, I don't know %s.", parts[1])
- p.Bot.SendMessage(channel, msg)
+ p.Bot.Send(c, bot.Message, channel, msg)
}
} else if len(parts) == 1 {
- p.reportCount(nick, channel, true)
+ p.reportCount(c, nick, channel, true)
}
// no matter what, if we're in here, then we've responded
return true
} else if parts[0] == "puke" {
- p.puke(nick, channel)
+ p.puke(c, nick, channel)
return true
}
if message.Command && parts[0] == "imbibe" {
p.addBeers(nick, 1)
- p.randomReply(channel)
+ p.randomReply(c, channel)
return true
}
@@ -135,7 +134,7 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel := message.Channel
if len(parts) < 2 {
- p.Bot.SendMessage(channel, "You must also provide a user name.")
+ p.Bot.Send(c, bot.Message, channel, "You must also provide a user name.")
} else if len(parts) == 3 {
chanNick = parts[2]
} else if len(parts) == 4 {
@@ -148,16 +147,19 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel: channel,
}
- log.Println("Creating Untappd user:", u.untappdUser, "nick:", u.chanNick)
+ log.Info().
+ Str("untappdUser", u.untappdUser).
+ Str("nick", u.chanNick).
+ Msg("Creating Untappd user")
var count int
err := p.db.QueryRow(`select count(*) from untappd
where untappdUser = ?`, u.untappdUser).Scan(&count)
if err != nil {
- log.Println("Error registering untappd: ", err)
+ log.Error().Err(err).Msgf("Error registering untappd")
}
if count > 0 {
- p.Bot.SendMessage(channel, "I'm already watching you.")
+ p.Bot.Send(c, bot.Message, channel, "I'm already watching you.")
return true
}
_, err = p.db.Exec(`insert into untappd (
@@ -172,45 +174,36 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
u.chanNick,
)
if err != nil {
- log.Println("Error registering untappd: ", err)
- p.Bot.SendMessage(channel, "I can't see.")
+ log.Error().Err(err).Msgf("Error registering untappd")
+ p.Bot.Send(c, bot.Message, channel, "I can't see.")
return true
}
- p.Bot.SendMessage(channel, "I'll be watching you.")
+ p.Bot.Send(c, bot.Message, channel, "I'll be watching you.")
- p.checkUntappd(channel)
+ p.checkUntappd(c, channel)
return true
}
if message.Command && parts[0] == "checkuntappd" {
- log.Println("Checking untappd at request of user.")
- p.checkUntappd(channel)
+ log.Info().
+ Str("user", message.User.Name).
+ Msgf("Checking untappd at request of user.")
+ p.checkUntappd(c, channel)
return true
}
return false
}
-// Empty event handler because this plugin does not do anything on event recv
-func (p *BeersPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// 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 *BeersPlugin) LoadData() {
- rand.Seed(time.Now().Unix())
-}
-
// Help responds to help requests. Every plugin must implement a help function.
-func (p *BeersPlugin) Help(channel string, parts []string) {
+func (p *BeersPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
msg := "Beers: imbibe by using either beers +=,=,++ or with the !imbibe/drink " +
"commands. I'll keep a count of how many beers you've had and then if you want " +
"to reset, just !puke it all up!"
- p.Bot.SendMessage(channel, msg)
+ p.Bot.Send(c, bot.Message, message.Channel, msg)
+ return true
}
func getUserBeers(db *sqlx.DB, user string) counter.Item {
@@ -222,7 +215,7 @@ func (p *BeersPlugin) setBeers(user string, amount int) {
ub := getUserBeers(p.db, user)
err := ub.Update(amount)
if err != nil {
- log.Println("Error saving beers: ", err)
+ log.Error().Err(err).Msgf("Error saving beers")
}
}
@@ -230,7 +223,7 @@ func (p *BeersPlugin) addBeers(user string, delta int) {
ub := getUserBeers(p.db, user)
err := ub.UpdateDelta(delta)
if err != nil {
- log.Println("Error saving beers: ", err)
+ log.Error().Err(err).Msgf("Error saving beers")
}
}
@@ -239,7 +232,7 @@ func (p *BeersPlugin) getBeers(nick string) int {
return ub.Count
}
-func (p *BeersPlugin) reportCount(nick, channel string, himself bool) {
+func (p *BeersPlugin) reportCount(c bot.Connector, nick, channel string, himself bool) {
beers := p.getBeers(nick)
msg := fmt.Sprintf("%s has had %d beers so far.", nick, beers)
if himself {
@@ -249,13 +242,13 @@ func (p *BeersPlugin) reportCount(nick, channel string, himself bool) {
msg = fmt.Sprintf("You've had %d beers so far, %s.", beers, nick)
}
}
- p.Bot.SendMessage(channel, msg)
+ p.Bot.Send(c, bot.Message, channel, msg)
}
-func (p *BeersPlugin) puke(user string, channel string) {
+func (p *BeersPlugin) puke(c bot.Connector, user string, channel string) {
p.setBeers(user, 0)
msg := fmt.Sprintf("Ohhhhhh, and a reversal of fortune for %s!", user)
- p.Bot.SendMessage(channel, msg)
+ p.Bot.Send(c, bot.Message, channel, msg)
}
func (p *BeersPlugin) doIKnow(nick string) bool {
@@ -268,9 +261,9 @@ func (p *BeersPlugin) doIKnow(nick string) bool {
}
// Sends random affirmation to the channel. This could be better (with a datastore for sayings)
-func (p *BeersPlugin) randomReply(channel string) {
+func (p *BeersPlugin) randomReply(c bot.Connector, channel string) {
replies := []string{"ZIGGY! ZAGGY!", "HIC!", "Stay thirsty, my friend!"}
- p.Bot.SendMessage(channel, replies[rand.Intn(len(replies))])
+ p.Bot.Send(c, bot.Message, channel, replies[rand.Intn(len(replies))])
}
type checkin struct {
@@ -316,7 +309,12 @@ type Beers struct {
}
func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
- access_token := "?access_token=" + p.Bot.Config().Untappd.Token
+ token := p.Bot.Config().Get("Untappd.Token", "NONE")
+ if token == "NONE" {
+ return []checkin{}, fmt.Errorf("No untappd token")
+ }
+
+ access_token := "?access_token=" + token
baseUrl := "https://api.untappd.com/v4/checkin/recent/"
url := baseUrl + access_token + "&limit=25"
@@ -332,55 +330,54 @@ func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
}
if resp.StatusCode == 500 {
- log.Printf("Error querying untappd: %s, %s", resp.Status, body)
+ log.Error().Msgf("Error querying untappd: %s, %s", resp.Status, body)
return []checkin{}, errors.New(resp.Status)
}
var beers Beers
err = json.Unmarshal(body, &beers)
if err != nil {
- log.Println(err)
+ log.Error().Err(err)
return []checkin{}, err
}
return beers.Response.Checkins.Items, nil
}
-func (p *BeersPlugin) checkUntappd(channel string) {
- token := p.Bot.Config().Untappd.Token
- if token == "" || token == "" {
- log.Println("No Untappd token, cannot enable plugin.")
+func (p *BeersPlugin) checkUntappd(c bot.Connector, channel string) {
+ token := p.Bot.Config().Get("Untappd.Token", "NONE")
+ if token == "NONE" {
+ log.Info().
+ Msg(`Set config value "untappd.token" if you wish to enable untappd`)
return
}
userMap := make(map[string]untappdUser)
rows, err := p.db.Query(`select id, untappdUser, channel, lastCheckin, chanNick from untappd;`)
if err != nil {
- log.Println("Error getting untappd users: ", err)
+ log.Error().Err(err).Msg("Error getting untappd users")
return
}
for rows.Next() {
u := untappdUser{}
err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick)
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
userMap[u.untappdUser] = u
- log.Printf("Found untappd user: %#v", u)
if u.chanNick == "" {
- log.Fatal("Empty chanNick for no good reason.")
+ log.Fatal().Msg("Empty chanNick for no good reason.")
}
}
chks, err := p.pullUntappd()
if err != nil {
- log.Println("Untappd ERROR: ", err)
+ log.Error().Err(err).Msg("Untappd ERROR")
return
}
for i := len(chks); i > 0; i-- {
checkin := chks[i-1]
if checkin.Checkin_id <= userMap[checkin.User.User_name].lastCheckin {
- log.Printf("User %s already check in >%d", checkin.User.User_name, checkin.Checkin_id)
continue
}
@@ -395,8 +392,9 @@ func (p *BeersPlugin) checkUntappd(channel string) {
if !ok {
continue
}
- log.Printf("user.chanNick: %s, user.untappdUser: %s, checkin.User.User_name: %s",
- user.chanNick, user.untappdUser, checkin.User.User_name)
+ log.Debug().
+ Msgf("user.chanNick: %s, user.untappdUser: %s, checkin.User.User_name: %s",
+ user.chanNick, user.untappdUser, checkin.User.User_name)
p.addBeers(user.chanNick, 1)
drunken := p.getBeers(user.chanNick)
@@ -410,11 +408,18 @@ func (p *BeersPlugin) checkUntappd(channel string) {
msg, checkin.Checkin_comment)
}
+ args := []interface{}{
+ channel,
+ msg,
+ }
if checkin.Media.Count > 0 {
if strings.Contains(checkin.Media.Items[0].Photo.Photo_img_lg, "photos-processing") {
continue
}
- msg += "\nHere's a photo: " + checkin.Media.Items[0].Photo.Photo_img_lg
+ args = append(args, bot.ImageAttachment{
+ URL: checkin.Media.Items[0].Photo.Photo_img_lg,
+ AltTxt: "Here's a photo",
+ })
}
user.lastCheckin = checkin.Checkin_id
@@ -422,33 +427,27 @@ func (p *BeersPlugin) checkUntappd(channel string) {
lastCheckin = ?
where id = ?`, user.lastCheckin, user.id)
if err != nil {
- log.Println("UPDATE ERROR!:", err)
+ log.Error().Err(err).Msg("UPDATE ERROR!")
}
- log.Println("checkin id:", checkin.Checkin_id, "Message:", msg)
- p.Bot.SendMessage(channel, msg)
+ log.Debug().
+ Int("checkin_id", checkin.Checkin_id).
+ Str("msg", msg).
+ Msg("checkin")
+ p.Bot.Send(c, bot.Message, args...)
}
}
-func (p *BeersPlugin) untappdLoop(channel string) {
- frequency := p.Bot.Config().Untappd.Freq
+func (p *BeersPlugin) untappdLoop(c bot.Connector, channel string) {
+ frequency := p.Bot.Config().GetInt("Untappd.Freq", 120)
+ if frequency == 0 {
+ return
+ }
- log.Println("Checking every ", frequency, " seconds")
+ log.Info().Msgf("Checking every %v seconds", frequency)
for {
time.Sleep(time.Duration(frequency) * time.Second)
- p.checkUntappd(channel)
+ p.checkUntappd(c, channel)
}
}
-
-// Handler for bot's own messages
-func (p *BeersPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *BeersPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *BeersPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/beers/beers_test.go b/plugins/beers/beers_test.go
index 8d14d86..d2c3ba2 100644
--- a/plugins/beers/beers_test.go
+++ b/plugins/beers/beers_test.go
@@ -3,6 +3,7 @@
package beers
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -13,12 +14,13 @@ import (
"github.com/velour/catbase/plugins/counter"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ c := &cli.CliPlugin{}
+ return c, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -29,18 +31,29 @@ func makeMessage(payload string) msg.Message {
func makeBeersPlugin(t *testing.T) (*BeersPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
counter.New(mb)
+ mb.DB().MustExec(`delete from counter; delete from counter_alias;`)
b := New(mb)
- assert.NotNil(t, b)
- b.Message(makeMessage("!mkalias beer :beer:"))
- b.Message(makeMessage("!mkalias beers :beer:"))
+ b.message(makeMessage("!mkalias beer :beer:"))
+ b.message(makeMessage("!mkalias beers :beer:"))
return b, mb
}
+func TestCounter(t *testing.T) {
+ _, mb := makeBeersPlugin(t)
+ i, err := counter.GetItem(mb.DB(), "tester", "test")
+ if !assert.Nil(t, err) {
+ t.Log(err)
+ t.Fatal()
+ }
+ err = i.Update(5)
+ assert.Nil(t, err)
+}
+
func TestImbibe(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("!imbibe"))
+ b.message(makeMessage("!imbibe"))
assert.Len(t, mb.Messages, 1)
- b.Message(makeMessage("!imbibe"))
+ b.message(makeMessage("!imbibe"))
assert.Len(t, mb.Messages, 2)
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
@@ -48,7 +61,7 @@ func TestImbibe(t *testing.T) {
}
func TestEq(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("!beers = 3"))
+ b.message(makeMessage("!beers = 3"))
assert.Len(t, mb.Messages, 1)
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
@@ -57,7 +70,7 @@ func TestEq(t *testing.T) {
func TestEqNeg(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("!beers = -3"))
+ b.message(makeMessage("!beers = -3"))
assert.Len(t, mb.Messages, 1)
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
@@ -66,8 +79,8 @@ func TestEqNeg(t *testing.T) {
func TestEqZero(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("beers += 5"))
- b.Message(makeMessage("!beers = 0"))
+ b.message(makeMessage("beers += 5"))
+ b.message(makeMessage("!beers = 0"))
assert.Len(t, mb.Messages, 2)
assert.Contains(t, mb.Messages[1], "reversal of fortune")
it, err := counter.GetItem(mb.DB(), "tester", itemName)
@@ -77,9 +90,9 @@ func TestEqZero(t *testing.T) {
func TestBeersPlusEq(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("beers += 5"))
+ b.message(makeMessage("beers += 5"))
assert.Len(t, mb.Messages, 1)
- b.Message(makeMessage("beers += 5"))
+ b.message(makeMessage("beers += 5"))
assert.Len(t, mb.Messages, 2)
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
@@ -88,11 +101,11 @@ func TestBeersPlusEq(t *testing.T) {
func TestPuke(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("beers += 5"))
+ b.message(makeMessage("beers += 5"))
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
assert.Equal(t, 5, it.Count)
- b.Message(makeMessage("puke"))
+ b.message(makeMessage("puke"))
it, err = counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
assert.Equal(t, 0, it.Count)
@@ -100,31 +113,16 @@ func TestPuke(t *testing.T) {
func TestBeersReport(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Message(makeMessage("beers += 5"))
+ b.message(makeMessage("beers += 5"))
it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err)
assert.Equal(t, 5, it.Count)
- b.Message(makeMessage("beers"))
+ b.message(makeMessage("beers"))
assert.Contains(t, mb.Messages[1], "5 beers")
}
func TestHelp(t *testing.T) {
b, mb := makeBeersPlugin(t)
- b.Help("channel", []string{})
+ b.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- b, _ := makeBeersPlugin(t)
- assert.False(t, b.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- b, _ := makeBeersPlugin(t)
- assert.False(t, b.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- b, _ := makeBeersPlugin(t)
- assert.Nil(t, b.RegisterWeb())
-}
diff --git a/plugins/cli/cli.go b/plugins/cli/cli.go
new file mode 100644
index 0000000..55d8875
--- /dev/null
+++ b/plugins/cli/cli.go
@@ -0,0 +1,117 @@
+// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
+
+package cli
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+ "html/template"
+ "net/http"
+ "time"
+)
+
+type CliPlugin struct {
+ bot bot.Bot
+ db *sqlx.DB
+ cache string
+ counter int
+}
+
+func New(b bot.Bot) *CliPlugin {
+ cp := &CliPlugin{
+ bot: b,
+ }
+ cp.registerWeb()
+ return cp
+}
+
+func (p *CliPlugin) registerWeb() {
+ http.HandleFunc("/cli/api", p.handleWebAPI)
+ http.HandleFunc("/cli", p.handleWeb)
+ p.bot.RegisterWeb("/cli", "CLI")
+}
+
+func (p *CliPlugin) handleWebAPI(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ fmt.Fprintf(w, "Incorrect HTTP method")
+ return
+ }
+ info := struct {
+ User string `json:"user"`
+ Payload string `json:"payload"`
+ Password string `json:"password"`
+ }{}
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&info)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ log.Debug().
+ Interface("postbody", info).
+ Msg("Got a POST")
+ if info.Password != p.bot.GetPassword() {
+ w.WriteHeader(http.StatusForbidden)
+ j, _ := json.Marshal(struct{ Err string }{Err: "Invalid Password"})
+ w.Write(j)
+ return
+ }
+
+ p.bot.Receive(p, bot.Message, msg.Message{
+ User: &user.User{
+ ID: info.User,
+ Name: info.User,
+ Admin: false,
+ },
+ Channel: "web",
+ Body: info.Payload,
+ Raw: info.Payload,
+ Command: true,
+ Time: time.Now(),
+ })
+
+ info.User = p.bot.WhoAmI()
+ info.Payload = p.cache
+ p.cache = ""
+
+ data, err := json.Marshal(info)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ w.Write(data)
+}
+
+var tpl = template.Must(template.New("factoidIndex").Parse(indexHTML))
+
+func (p *CliPlugin) handleWeb(w http.ResponseWriter, r *http.Request) {
+ tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.bot.GetWebNavigation()})
+}
+
+// Completing the Connector interface, but will not actually be a connector
+func (p *CliPlugin) RegisterEvent(cb bot.Callback) {}
+func (p *CliPlugin) Send(kind bot.Kind, args ...interface{}) (string, error) {
+ switch kind {
+ case bot.Message:
+ fallthrough
+ case bot.Action:
+ fallthrough
+ case bot.Reply:
+ fallthrough
+ case bot.Reaction:
+ p.cache += args[1].(string) + "\n"
+ }
+ id := fmt.Sprintf("%d", p.counter)
+ p.counter++
+ return id, nil
+}
+func (p *CliPlugin) GetEmojiList() map[string]string { return nil }
+func (p *CliPlugin) Serve() error { return nil }
+func (p *CliPlugin) Who(s string) []string { return nil }
diff --git a/plugins/cli/index.go b/plugins/cli/index.go
new file mode 100644
index 0000000..63e9079
--- /dev/null
+++ b/plugins/cli/index.go
@@ -0,0 +1,129 @@
+package cli
+
+var indexHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CLI
+
+
+
+
+
+ CLI
+
+ {{ "{{ item.Name }}" }}
+
+
+
+ {{ "{{ err }}" }}
+
+
+
+ Password:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+`
diff --git a/plugins/couldashouldawoulda/csw.go b/plugins/couldashouldawoulda/csw.go
index 455c45d..c424fba 100644
--- a/plugins/couldashouldawoulda/csw.go
+++ b/plugins/couldashouldawoulda/csw.go
@@ -17,13 +17,15 @@ type CSWPlugin struct {
Config *config.Config
}
-func New(bot bot.Bot) *CSWPlugin {
- return &CSWPlugin{
- Bot: bot,
+func New(b bot.Bot) *CSWPlugin {
+ csw := &CSWPlugin{
+ Bot: b,
}
+ b.Register(csw, bot.Message, csw.message)
+ return csw
}
-func (p *CSWPlugin) Message(message msg.Message) bool {
+func (p *CSWPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@@ -63,25 +65,9 @@ func (p *CSWPlugin) Message(message msg.Message) bool {
}
}
- p.Bot.SendMessage(message.Channel, responses[rand.Intn(len(responses))])
+ p.Bot.Send(c, bot.Message, message.Channel, responses[rand.Intn(len(responses))])
return true
}
return false
}
-
-func (p *CSWPlugin) Help(channel string, parts []string) {}
-
-func (p *CSWPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *CSWPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *CSWPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
-
-func (p *CSWPlugin) RegisterWeb() *string {
- return nil
-}
diff --git a/plugins/couldashouldawoulda/csw_test.go b/plugins/couldashouldawoulda/csw_test.go
index 73b5261..e647ede 100644
--- a/plugins/couldashouldawoulda/csw_test.go
+++ b/plugins/couldashouldawoulda/csw_test.go
@@ -3,6 +3,7 @@
package couldashouldawoulda
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -29,7 +30,7 @@ func Test0(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!should I drink a beer?"))
+ res := c.message(makeMessage("!should I drink a beer?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@@ -47,7 +48,7 @@ func Test1(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!should I drink a beer or a bourbon?"))
+ res := c.message(makeMessage("!should I drink a beer or a bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"}
@@ -65,7 +66,7 @@ func Test2(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!could I drink a beer or a bourbon?"))
+ res := c.message(makeMessage("!could I drink a beer or a bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@@ -83,7 +84,7 @@ func Test3(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!would I die if I drank too much bourbon?"))
+ res := c.message(makeMessage("!would I die if I drank too much bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@@ -101,7 +102,7 @@ func Test4(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!would I die or be sick if I drank all the bourbon?"))
+ res := c.message(makeMessage("!would I die or be sick if I drank all the bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"}
@@ -119,7 +120,7 @@ func Test5(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!should I have another beer or bourbon or tequila?"))
+ res := c.message(makeMessage("!should I have another beer or bourbon or tequila?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"I'd say option", "You'd be an idiot not to choose the"}
diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go
index 4ad9a41..309ff47 100644
--- a/plugins/counter/counter.go
+++ b/plugins/counter/counter.go
@@ -1,15 +1,18 @@
-// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
-
package counter
import (
"database/sql"
+ "encoding/json"
"fmt"
- "log"
+ "html/template"
+ "math/rand"
+ "net/http"
"regexp"
"strconv"
"strings"
+ "github.com/rs/zerolog/log"
+
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
@@ -41,6 +44,20 @@ type alias struct {
PointsTo string `db:"points_to"`
}
+// GetItems returns all counters
+func GetAllItems(db *sqlx.DB) ([]Item, error) {
+ var items []Item
+ err := db.Select(&items, `select * from counter`)
+ if err != nil {
+ return nil, err
+ }
+ // Don't forget to embed the DB into all of that shiz
+ for i := range items {
+ items[i].DB = db
+ }
+ return items, nil
+}
+
// GetItems returns all counters for a subject
func GetItems(db *sqlx.DB, nick string) ([]Item, error) {
var items []Item
@@ -111,7 +128,7 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil {
itemName = a.PointsTo
} else {
- log.Println(err, a)
+ log.Error().Err(err).Interface("alias", a)
}
err := db.Get(&item, `select * from counter where nick = ? and item= ?`,
@@ -125,7 +142,11 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
default:
return Item{}, err
}
- log.Printf("Got item %s.%s: %#v", nick, itemName, item)
+ log.Debug().
+ Str("nick", nick).
+ Str("itemName", itemName).
+ Interface("item", item).
+ Msg("got item")
return item, nil
}
@@ -133,6 +154,9 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
func (i *Item) Create() error {
res, err := i.Exec(`insert into counter (nick, item, count) values (?, ?, ?);`,
i.Nick, i.Item, i.Count)
+ if err != nil {
+ return err
+ }
id, _ := res.LastInsertId()
// hackhackhack?
i.ID = id
@@ -149,7 +173,10 @@ func (i *Item) Update(value int) error {
if i.ID == -1 {
i.Create()
}
- log.Printf("Updating item: %#v, value: %d", i, value)
+ log.Debug().
+ Interface("i", i).
+ Int("value", value).
+ Msg("Updating item")
_, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID)
return err
}
@@ -169,33 +196,35 @@ func (i *Item) Delete() error {
}
// NewCounterPlugin creates a new CounterPlugin with the Plugin interface
-func New(bot bot.Bot) *CounterPlugin {
- if _, err := bot.DB().Exec(`create table if not exists counter (
+func New(b bot.Bot) *CounterPlugin {
+ tx := b.DB().MustBegin()
+ b.DB().MustExec(`create table if not exists counter (
id integer primary key,
nick string,
item string,
count integer
- );`); err != nil {
- log.Fatal(err)
- }
- if _, err := bot.DB().Exec(`create table if not exists counter_alias (
+ );`)
+ b.DB().MustExec(`create table if not exists counter_alias (
id integer PRIMARY KEY AUTOINCREMENT,
item string NOT NULL UNIQUE,
points_to string NOT NULL
- );`); err != nil {
- log.Fatal(err)
- }
- return &CounterPlugin{
- Bot: bot,
- DB: bot.DB(),
+ );`)
+ tx.Commit()
+ cp := &CounterPlugin{
+ Bot: b,
+ DB: b.DB(),
}
+ b.Register(cp, bot.Message, cp.message)
+ b.Register(cp, bot.Help, cp.help)
+ cp.registerWeb()
+ return cp
}
// 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 *CounterPlugin) Message(message msg.Message) bool {
+func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
// This bot does not reply to anything
nick := message.User.Name
channel := message.Channel
@@ -207,10 +236,10 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
if len(parts) == 3 && strings.ToLower(parts[0]) == "mkalias" {
if _, err := MkAlias(p.DB, parts[1], parts[2]); err != nil {
- log.Println(err)
+ log.Error().Err(err)
return false
}
- p.Bot.SendMessage(channel, fmt.Sprintf("Created alias %s -> %s",
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("Created alias %s -> %s",
parts[1], parts[2]))
return true
} else if strings.ToLower(parts[0]) == "leaderboard" {
@@ -226,7 +255,7 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
its, err := cmd()
if err != nil {
- log.Println(err)
+ log.Error().Err(err)
return false
} else if len(its) == 0 {
return false
@@ -240,23 +269,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it.Item,
)
}
- p.Bot.SendMessage(channel, out)
+ p.Bot.Send(c, bot.Message, channel, out)
return true
} else if match := teaMatcher.MatchString(message.Body); match {
// check for tea match TTT
- return p.checkMatch(message)
+ return p.checkMatch(c, message)
} else if message.Command && message.Body == "reset me" {
items, err := GetItems(p.DB, strings.ToLower(nick))
if err != nil {
- log.Printf("Error getting items to reset %s: %s", nick, err)
- p.Bot.SendMessage(channel, "Something is technically wrong with your counters.")
+ log.Error().
+ Err(err).
+ Str("nick", nick).
+ Msg("Error getting items to reset")
+ p.Bot.Send(c, bot.Message, channel, "Something is technically wrong with your counters.")
return true
}
- log.Printf("Items: %+v", items)
+ log.Debug().Msgf("Items: %+v", items)
for _, item := range items {
item.Delete()
}
- p.Bot.SendMessage(channel, fmt.Sprintf("%s, you are as new, my son.", nick))
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s, you are as new, my son.", nick))
return true
} else if message.Command && parts[0] == "inspect" && len(parts) == 2 {
var subject string
@@ -267,12 +299,17 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
subject = strings.ToLower(parts[1])
}
- log.Printf("Getting counter for %s", subject)
+ log.Debug().
+ Str("subject", subject).
+ Msg("Getting counter")
// pull all of the items associated with "subject"
items, err := GetItems(p.DB, subject)
if err != nil {
- log.Fatalf("Error retrieving items for %s: %s", subject, err)
- p.Bot.SendMessage(channel, "Something went wrong finding that counter;")
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Msg("Error retrieving items")
+ p.Bot.Send(c, bot.Message, channel, "Something went wrong finding that counter;")
return true
}
@@ -292,11 +329,11 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
resp += "."
if count == 0 {
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has no counters.", subject))
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has no counters.", subject))
return true
}
- p.Bot.SendMessage(channel, resp)
+ p.Bot.Send(c, bot.Message, channel, resp)
return true
} else if message.Command && len(parts) == 2 && parts[0] == "clear" {
subject := strings.ToLower(nick)
@@ -304,18 +341,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it, err := GetItem(p.DB, subject, itemName)
if err != nil {
- log.Printf("Error getting item to remove %s.%s: %s", subject, itemName, err)
- p.Bot.SendMessage(channel, "Something went wrong removing that counter;")
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error getting item to remove")
+ p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
return true
}
err = it.Delete()
if err != nil {
- log.Printf("Error removing item %s.%s: %s", subject, itemName, err)
- p.Bot.SendMessage(channel, "Something went wrong removing that counter;")
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error removing item")
+ p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
return true
}
- p.Bot.SendAction(channel, fmt.Sprintf("chops a few %s out of his brain",
+ p.Bot.Send(c, bot.Action, channel, fmt.Sprintf("chops a few %s out of his brain",
itemName))
return true
@@ -338,16 +383,19 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
item, err := GetItem(p.DB, subject, itemName)
switch {
case err == sql.ErrNoRows:
- p.Bot.SendMessage(channel, fmt.Sprintf("I don't think %s has any %s.",
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("I don't think %s has any %s.",
subject, itemName))
return true
case err != nil:
- log.Printf("Error retrieving item count for %s.%s: %s",
- subject, itemName, err)
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error retrieving item count")
return true
}
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject, item.Count,
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject, item.Count,
itemName))
return true
@@ -372,25 +420,33 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// ++ those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
- log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("error finding item")
// Item ain't there, I guess
return false
}
- log.Printf("About to update item: %#v", item)
+ log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1)
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
} else if strings.HasSuffix(parts[0], "--") {
// -- those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
- log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error finding item")
// Item ain't there, I guess
return false
}
item.UpdateDelta(-1)
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
}
@@ -412,28 +468,36 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// += those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
- log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error finding item")
// Item ain't there, I guess
return false
}
n, _ := strconv.Atoi(parts[2])
- log.Printf("About to update item by %d: %#v", n, item)
+ log.Debug().Msgf("About to update item by %d: %#v", n, item)
item.UpdateDelta(n)
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
} else if parts[1] == "-=" {
// -= those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
- log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
+ log.Error().
+ Err(err).
+ Str("subject", subject).
+ Str("itemName", itemName).
+ Msg("Error finding item")
// Item ain't there, I guess
return false
}
n, _ := strconv.Atoi(parts[2])
- log.Printf("About to update item by -%d: %#v", n, item)
+ log.Debug().Msgf("About to update item by -%d: %#v", n, item)
item.UpdateDelta(-n)
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
}
@@ -443,31 +507,15 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *CounterPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "You can set counters incrementally by using "+
+func (p *CounterPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.Bot.Send(c, bot.Message, message.Channel, "You can set counters incrementally by using "+
"++ and --. You can see all of your counters using "+
"\"inspect\", erase them with \"clear\", and view single counters with "+
"\"count\".")
+ return true
}
-// Empty event handler because this plugin does not do anything on event recv
-func (p *CounterPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *CounterPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *CounterPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *CounterPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
-
-func (p *CounterPlugin) checkMatch(message msg.Message) bool {
+func (p *CounterPlugin) checkMatch(c bot.Connector, message msg.Message) bool {
nick := message.User.Name
channel := message.Channel
@@ -480,13 +528,98 @@ func (p *CounterPlugin) checkMatch(message msg.Message) bool {
// We will specifically allow :tea: to keep compatability
item, err := GetItem(p.DB, nick, itemName)
if err != nil || (item.Count == 0 && item.Item != ":tea:") {
- log.Printf("Error finding item %s.%s: %s.", nick, itemName, err)
+ log.Error().
+ Err(err).
+ Str("itemName", itemName).
+ Msg("Error finding item")
// Item ain't there, I guess
return false
}
- log.Printf("About to update item: %#v", item)
+ log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1)
- p.Bot.SendMessage(channel, fmt.Sprintf("bleep-bloop-blop... %s has %d %s",
- nick, item.Count, itemName))
+ p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s... %s has %d %s",
+ strings.Join(everyDayImShuffling([]string{"bleep", "bloop", "blop"}), "-"), nick, item.Count, itemName))
return true
}
+
+func everyDayImShuffling(vals []string) []string {
+ ret := make([]string, len(vals))
+ perm := rand.Perm(len(vals))
+ for i, randIndex := range perm {
+ ret[i] = vals[randIndex]
+ }
+ return ret
+}
+
+func (p *CounterPlugin) registerWeb() {
+ http.HandleFunc("/counter/api", p.handleCounterAPI)
+ http.HandleFunc("/counter", p.handleCounter)
+ p.Bot.RegisterWeb("/counter", "Counter")
+}
+
+var tpl = template.Must(template.New("factoidIndex").Parse(html))
+
+func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
+ tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.Bot.GetWebNavigation()})
+}
+
+func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) {
+ if r.Method == http.MethodPost {
+ info := struct {
+ User string
+ Thing string
+ Action string
+ Password string
+ }{}
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&info)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ log.Debug().
+ Interface("postbody", info).
+ Msg("Got a POST")
+ if info.Password != p.Bot.GetPassword() {
+ w.WriteHeader(http.StatusForbidden)
+ j, _ := json.Marshal(struct{ Err string }{Err: "Invalid Password"})
+ w.Write(j)
+ return
+ }
+ item, err := GetItem(p.DB, info.User, info.Thing)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Str("subject", info.User).
+ Str("itemName", info.Thing).
+ Msg("error finding item")
+ w.WriteHeader(404)
+ fmt.Fprint(w, err)
+ return
+ }
+ if info.Action == "++" {
+ item.UpdateDelta(1)
+ } else if info.Action == "--" {
+ item.UpdateDelta(-1)
+ } else {
+ w.WriteHeader(400)
+ fmt.Fprint(w, "Invalid increment")
+ return
+ }
+
+ }
+ all, err := GetAllItems(p.DB)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ data, err := json.Marshal(all)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
+ fmt.Fprint(w, string(data))
+}
diff --git a/plugins/counter/counter_test.go b/plugins/counter/counter_test.go
index fff9ef0..99f9212 100644
--- a/plugins/counter/counter_test.go
+++ b/plugins/counter/counter_test.go
@@ -4,6 +4,7 @@ package counter
import (
"fmt"
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -16,17 +17,18 @@ import (
func setup(t *testing.T) (*bot.MockBot, *CounterPlugin) {
mb := bot.NewMockBot()
c := New(mb)
+ mb.DB().MustExec(`delete from counter; delete from counter_alias;`)
_, err := MkAlias(mb.DB(), "tea", ":tea:")
assert.Nil(t, err)
return mb, c
}
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -37,8 +39,8 @@ func makeMessage(payload string) msg.Message {
func TestThreeSentencesExists(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage(":beer:++"))
- c.Message(makeMessage(":beer:. Earl Grey. Hot."))
+ c.message(makeMessage(":beer:++"))
+ c.message(makeMessage(":beer:. Earl Grey. Hot."))
item, err := GetItem(mb.DB(), "tester", ":beer:")
assert.Nil(t, err)
assert.Equal(t, 2, item.Count)
@@ -48,7 +50,7 @@ func TestThreeSentencesNotExists(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
item, err := GetItem(mb.DB(), "tester", ":beer:")
- c.Message(makeMessage(":beer:. Earl Grey. Hot."))
+ c.message(makeMessage(":beer:. Earl Grey. Hot."))
item, err = GetItem(mb.DB(), "tester", ":beer:")
assert.Nil(t, err)
assert.Equal(t, 0, item.Count)
@@ -57,8 +59,8 @@ func TestThreeSentencesNotExists(t *testing.T) {
func TestTeaEarlGreyHot(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea. Earl Grey. Hot."))
- c.Message(makeMessage("Tea. Earl Grey. Hot."))
+ c.message(makeMessage("Tea. Earl Grey. Hot."))
+ c.message(makeMessage("Tea. Earl Grey. Hot."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 2, item.Count)
@@ -67,8 +69,8 @@ func TestTeaEarlGreyHot(t *testing.T) {
func TestTeaTwoPeriods(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea. Earl Grey."))
- c.Message(makeMessage("Tea. Earl Grey."))
+ c.message(makeMessage("Tea. Earl Grey."))
+ c.message(makeMessage("Tea. Earl Grey."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 0, item.Count)
@@ -77,8 +79,8 @@ func TestTeaTwoPeriods(t *testing.T) {
func TestTeaMultiplePeriods(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea. Earl Grey. Spiked. Hot."))
- c.Message(makeMessage("Tea. Earl Grey. Spiked. Hot."))
+ c.message(makeMessage("Tea. Earl Grey. Spiked. Hot."))
+ c.message(makeMessage("Tea. Earl Grey. Spiked. Hot."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 2, item.Count)
@@ -87,9 +89,9 @@ func TestTeaMultiplePeriods(t *testing.T) {
func TestTeaGreenHot(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea. Green. Hot."))
- c.Message(makeMessage("Tea. Green. Hot"))
- c.Message(makeMessage("Tea. Green. Iced."))
+ c.message(makeMessage("Tea. Green. Hot."))
+ c.message(makeMessage("Tea. Green. Hot"))
+ c.message(makeMessage("Tea. Green. Iced."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 3, item.Count)
@@ -98,8 +100,8 @@ func TestTeaGreenHot(t *testing.T) {
func TestTeaUnrelated(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea."))
- c.Message(makeMessage("Tea. It's great."))
+ c.message(makeMessage("Tea."))
+ c.message(makeMessage("Tea. It's great."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 0, item.Count)
@@ -108,7 +110,7 @@ func TestTeaUnrelated(t *testing.T) {
func TestTeaSkieselQuote(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("blah, this is a whole page of explanation where \"we did local search and used a tabu list\" would have sufficed"))
+ c.message(makeMessage("blah, this is a whole page of explanation where \"we did local search and used a tabu list\" would have sufficed"))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 0, item.Count)
@@ -116,7 +118,7 @@ func TestTeaSkieselQuote(t *testing.T) {
func TestTeaUnicodeJapanese(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("Tea. おちや. Hot."))
+ c.message(makeMessage("Tea. おちや. Hot."))
item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err)
assert.Equal(t, 1, item.Count)
@@ -125,8 +127,8 @@ func TestTeaUnicodeJapanese(t *testing.T) {
func TestResetMe(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("test++"))
- c.Message(makeMessage("!reset me"))
+ c.message(makeMessage("test++"))
+ c.message(makeMessage("!reset me"))
items, err := GetItems(mb.DB(), "tester")
assert.Nil(t, err)
assert.Len(t, items, 0)
@@ -135,7 +137,7 @@ func TestResetMe(t *testing.T) {
func TestCounterOne(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Len(t, mb.Messages, 1)
assert.Equal(t, mb.Messages[0], "tester has 1 test.")
}
@@ -143,7 +145,7 @@ func TestCounterOne(t *testing.T) {
func TestCounterOneWithSpace(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Message(makeMessage(":test: ++"))
+ c.message(makeMessage(":test: ++"))
assert.Len(t, mb.Messages, 1)
assert.Equal(t, mb.Messages[0], "tester has 1 :test:.")
}
@@ -152,7 +154,7 @@ func TestCounterFour(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
}
assert.Len(t, mb.Messages, 4)
assert.Equal(t, mb.Messages[3], "tester has 4 test.")
@@ -162,10 +164,10 @@ func TestCounterDecrement(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
}
- c.Message(makeMessage("test--"))
+ c.message(makeMessage("test--"))
assert.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "tester has 3 test.")
}
@@ -174,10 +176,10 @@ func TestFriendCounterDecrement(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("other.test++"))
+ c.message(makeMessage("other.test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("other has %d test.", i+1))
}
- c.Message(makeMessage("other.test--"))
+ c.message(makeMessage("other.test--"))
assert.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "other has 3 test.")
}
@@ -186,12 +188,12 @@ func TestDecrementZero(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
}
j := 4
for i := 4; i > 0; i-- {
- c.Message(makeMessage("test--"))
+ c.message(makeMessage("test--"))
assert.Equal(t, mb.Messages[j], fmt.Sprintf("tester has %d test.", i-1))
j++
}
@@ -203,10 +205,10 @@ func TestClear(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
}
- res := c.Message(makeMessage("!clear test"))
+ res := c.message(makeMessage("!clear test"))
assert.True(t, res)
assert.Len(t, mb.Actions, 1)
assert.Equal(t, mb.Actions[0], "chops a few test out of his brain")
@@ -216,10 +218,10 @@ func TestCount(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
}
- res := c.Message(makeMessage("!count test"))
+ res := c.message(makeMessage("!count test"))
assert.True(t, res)
assert.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "tester has 4 test.")
@@ -229,18 +231,18 @@ func TestInspectMe(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
for i := 0; i < 4; i++ {
- c.Message(makeMessage("test++"))
+ c.message(makeMessage("test++"))
assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
}
for i := 0; i < 2; i++ {
- c.Message(makeMessage("fucks++"))
+ c.message(makeMessage("fucks++"))
assert.Equal(t, mb.Messages[i+4], fmt.Sprintf("tester has %d fucks.", i+1))
}
for i := 0; i < 20; i++ {
- c.Message(makeMessage("cheese++"))
+ c.message(makeMessage("cheese++"))
assert.Equal(t, mb.Messages[i+6], fmt.Sprintf("tester has %d cheese.", i+1))
}
- res := c.Message(makeMessage("!inspect me"))
+ res := c.message(makeMessage("!inspect me"))
assert.True(t, res)
assert.Len(t, mb.Messages, 27)
assert.Equal(t, mb.Messages[26], "tester has the following counters: test: 4, fucks: 2, cheese: 20.")
@@ -249,24 +251,6 @@ func TestInspectMe(t *testing.T) {
func TestHelp(t *testing.T) {
mb, c := setup(t)
assert.NotNil(t, c)
- c.Help("channel", []string{})
+ c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- _, c := setup(t)
- assert.NotNil(t, c)
- assert.False(t, c.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- _, c := setup(t)
- assert.NotNil(t, c)
- assert.False(t, c.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- _, c := setup(t)
- assert.NotNil(t, c)
- assert.Nil(t, c.RegisterWeb())
-}
diff --git a/plugins/counter/html.go b/plugins/counter/html.go
new file mode 100644
index 0000000..dd75703
--- /dev/null
+++ b/plugins/counter/html.go
@@ -0,0 +1,103 @@
+package counter
+
+var html = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Counters
+
+
+
+
+
+ Counters
+
+ {{ "{{ item.Name }}" }}
+
+
+
+ {{ "{{ err }}" }}
+
+
+
+ Password:
+
+
+
+ {{ "{{ user }}" }}:
+
+
+
+ {{ "{{ thing }}" }}:
+
+
+ {{ "{{ count }}" }}
+
+
+ -
+ +
+
+
+
+
+
+
+
+
+
+
+`
diff --git a/plugins/db/db.go b/plugins/db/db.go
deleted file mode 100644
index bff767b..0000000
--- a/plugins/db/db.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package db
-
-import (
- "fmt"
- "log"
- "net/http"
- "os"
- "time"
-
- "github.com/velour/catbase/bot"
- "github.com/velour/catbase/bot/msg"
- "github.com/velour/catbase/config"
-)
-
-type DBPlugin struct {
- bot bot.Bot
- config *config.Config
-}
-
-func New(b bot.Bot) *DBPlugin {
- return &DBPlugin{b, b.Config()}
-}
-
-func (p *DBPlugin) Message(message msg.Message) bool { return false }
-func (p *DBPlugin) Event(kind string, message msg.Message) bool { return false }
-func (p *DBPlugin) ReplyMessage(msg.Message, string) bool { return false }
-func (p *DBPlugin) BotMessage(message msg.Message) bool { return false }
-func (p *DBPlugin) Help(channel string, parts []string) {}
-
-func (p *DBPlugin) RegisterWeb() *string {
- http.HandleFunc("/db/catbase.db", p.serveQuery)
- tmp := "/db/catbase.db"
- return &tmp
-}
-
-func (p *DBPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
- f, err := os.Open(p.bot.Config().DB.File)
- defer f.Close()
- if err != nil {
- log.Printf("Error opening DB for web service: %s", err)
- fmt.Fprintf(w, "Error opening DB")
- return
- }
- http.ServeContent(w, r, "catbase.db", time.Now(), f)
-}
diff --git a/plugins/dice/dice.go b/plugins/dice/dice.go
index 104e016..b171820 100644
--- a/plugins/dice/dice.go
+++ b/plugins/dice/dice.go
@@ -19,10 +19,13 @@ type DicePlugin struct {
}
// NewDicePlugin creates a new DicePlugin with the Plugin interface
-func New(bot bot.Bot) *DicePlugin {
- return &DicePlugin{
- Bot: bot,
+func New(b bot.Bot) *DicePlugin {
+ dp := &DicePlugin{
+ Bot: b,
}
+ b.Register(dp, bot.Message, dp.message)
+ b.Register(dp, bot.Help, dp.help)
+ return dp
}
func rollDie(sides int) int {
@@ -32,7 +35,7 @@ func rollDie(sides int) int {
// 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 *DicePlugin) Message(message msg.Message) bool {
+func (p *DicePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@@ -46,7 +49,7 @@ func (p *DicePlugin) Message(message msg.Message) bool {
}
if sides < 2 || nDice < 1 || nDice > 20 {
- p.Bot.SendMessage(channel, "You're a dick.")
+ p.Bot.Send(c, bot.Message, channel, "You're a dick.")
return true
}
@@ -61,29 +64,13 @@ func (p *DicePlugin) Message(message msg.Message) bool {
}
}
- p.Bot.SendMessage(channel, rolls)
+ p.Bot.Send(c, bot.Message, channel, rolls)
return true
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *DicePlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Roll dice using notation XdY. Try \"3d20\".")
+func (p *DicePlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.Bot.Send(c, bot.Message, message.Channel, "Roll dice using notation XdY. Try \"3d20\".")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *DicePlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *DicePlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *DicePlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *DicePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/dice/dice_test.go b/plugins/dice/dice_test.go
index ec060de..96eeb29 100644
--- a/plugins/dice/dice_test.go
+++ b/plugins/dice/dice_test.go
@@ -3,6 +3,7 @@
package dice
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -29,7 +30,7 @@ func TestDie(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!1d6"))
+ res := c.message(makeMessage("!1d6"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "tester, you rolled:")
@@ -39,7 +40,7 @@ func TestDice(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!5d6"))
+ res := c.message(makeMessage("!5d6"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "tester, you rolled:")
@@ -49,7 +50,7 @@ func TestNotCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("1d6"))
+ res := c.message(makeMessage("1d6"))
assert.False(t, res)
assert.Len(t, mb.Messages, 0)
}
@@ -58,7 +59,7 @@ func TestBadDice(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!aued6"))
+ res := c.message(makeMessage("!aued6"))
assert.False(t, res)
assert.Len(t, mb.Messages, 0)
}
@@ -67,7 +68,7 @@ func TestBadSides(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!1daoeu"))
+ res := c.message(makeMessage("!1daoeu"))
assert.False(t, res)
assert.Len(t, mb.Messages, 0)
}
@@ -76,7 +77,7 @@ func TestLotsOfDice(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!100d100"))
+ res := c.message(makeMessage("!100d100"))
assert.True(t, res)
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "You're a dick.")
@@ -86,27 +87,6 @@ func TestHelp(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- c.Help("channel", []string{})
+ c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.Nil(t, c.RegisterWeb())
-}
diff --git a/plugins/downtime/downtime.go b/plugins/downtime/downtime.go
deleted file mode 100644
index ef9df9e..0000000
--- a/plugins/downtime/downtime.go
+++ /dev/null
@@ -1,235 +0,0 @@
-// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
-
-package downtime
-
-import (
- "database/sql"
-
- "github.com/jmoiron/sqlx"
- "github.com/velour/catbase/bot"
- "github.com/velour/catbase/bot/msg"
-)
-
-import (
- "fmt"
- "log"
- "sort"
- "strings"
- "time"
-)
-
-// This is a downtime plugin to monitor how much our users suck
-
-type DowntimePlugin struct {
- Bot bot.Bot
- db *sqlx.DB
-}
-
-type idleEntry struct {
- id sql.NullInt64
- nick string
- lastSeen time.Time
-}
-
-func (entry idleEntry) saveIdleEntry(db *sqlx.DB) error {
- var err error
- if entry.id.Valid {
- log.Println("Updating downtime for: ", entry)
- _, err = db.Exec(`update downtime set
- nick=?, lastSeen=?
- where id=?;`, entry.nick, entry.lastSeen.Unix(), entry.id.Int64)
- } else {
- log.Println("Inserting downtime for: ", entry)
- _, err = db.Exec(`insert into downtime (nick, lastSeen)
- values (?, ?)`, entry.nick, entry.lastSeen.Unix())
- }
- return err
-}
-
-func getIdleEntryByNick(db *sqlx.DB, nick string) (idleEntry, error) {
- var id sql.NullInt64
- var lastSeen sql.NullInt64
- err := db.QueryRow(`select id, max(lastSeen) from downtime
- where nick = ?`, nick).Scan(&id, &lastSeen)
- if err != nil {
- log.Println("Error selecting downtime: ", err)
- return idleEntry{}, err
- }
- if !id.Valid {
- return idleEntry{
- nick: nick,
- lastSeen: time.Now(),
- }, nil
- }
- return idleEntry{
- id: id,
- nick: nick,
- lastSeen: time.Unix(lastSeen.Int64, 0),
- }, nil
-}
-
-func getAllIdleEntries(db *sqlx.DB) (idleEntries, error) {
- rows, err := db.Query(`select id, nick, max(lastSeen) from downtime
- group by nick`)
- if err != nil {
- return nil, err
- }
- entries := idleEntries{}
- for rows.Next() {
- var e idleEntry
- err := rows.Scan(&e.id, &e.nick, &e.lastSeen)
- if err != nil {
- return nil, err
- }
- entries = append(entries, &e)
- }
- return entries, nil
-}
-
-type idleEntries []*idleEntry
-
-func (ie idleEntries) Len() int {
- return len(ie)
-}
-
-func (ie idleEntries) Less(i, j int) bool {
- return ie[i].lastSeen.Before(ie[j].lastSeen)
-}
-
-func (ie idleEntries) Swap(i, j int) {
- ie[i], ie[j] = ie[j], ie[i]
-}
-
-// NewDowntimePlugin creates a new DowntimePlugin with the Plugin interface
-func New(bot bot.Bot) *DowntimePlugin {
- p := DowntimePlugin{
- Bot: bot,
- db: bot.DB(),
- }
-
- if bot.DBVersion() == 1 {
- _, err := p.db.Exec(`create table if not exists downtime (
- id integer primary key,
- nick string,
- lastSeen integer
- );`)
- if err != nil {
- log.Fatal("Error creating downtime table: ", err)
- }
- }
-
- 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 *DowntimePlugin) Message(message msg.Message) bool {
- // If it's a command and the payload is idle , give it. Log everything.
-
- parts := strings.Fields(strings.ToLower(message.Body))
- channel := message.Channel
- ret := false
-
- if len(parts) == 0 {
- return false
- }
-
- if parts[0] == "idle" && len(parts) == 2 {
- nick := parts[1]
- // parts[1] must be the userid, or we don't know them
- entry, err := getIdleEntryByNick(p.db, nick)
- if err != nil {
- log.Println("Error getting idle entry: ", err)
- }
- if !entry.id.Valid {
- // couldn't find em
- p.Bot.SendMessage(channel, fmt.Sprintf("Sorry, I don't know %s.", nick))
- } else {
- p.Bot.SendMessage(channel, fmt.Sprintf("%s has been idle for: %s",
- nick, time.Now().Sub(entry.lastSeen)))
- }
- ret = true
- } else if parts[0] == "idle" && len(parts) == 1 {
- // Find all idle times, report them.
- entries, err := getAllIdleEntries(p.db)
- if err != nil {
- log.Println("Error retrieving idle entries: ", err)
- }
- sort.Sort(entries)
- tops := "The top entries are: "
- for _, e := range entries {
-
- // filter out ZNC entries and ourself
- if strings.HasPrefix(e.nick, "*") || strings.ToLower(p.Bot.Config().Nick) == e.nick {
- p.remove(e.nick)
- } else {
- tops = fmt.Sprintf("%s%s: %s ", tops, e.nick, time.Now().Sub(e.lastSeen))
- }
- }
- p.Bot.SendMessage(channel, tops)
- ret = true
-
- }
-
- p.record(strings.ToLower(message.User.Name))
-
- return ret
-}
-
-func (p *DowntimePlugin) record(user string) {
- entry, err := getIdleEntryByNick(p.db, user)
- if err != nil {
- log.Println("Error recording downtime: ", err)
- }
- entry.lastSeen = time.Now()
- entry.saveIdleEntry(p.db)
- log.Println("Inserted downtime for:", user)
-}
-
-func (p *DowntimePlugin) remove(user string) error {
- _, err := p.db.Exec(`delete from downtime where nick = ?`, user)
- if err != nil {
- log.Println("Error removing downtime for user: ", user, err)
- return err
- }
- log.Println("Removed downtime for:", user)
- return nil
-}
-
-// Help responds to help requests. Every plugin must implement a help function.
-func (p *DowntimePlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Ask me how long one of your friends has been idele with, \"idle \"")
-}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *DowntimePlugin) Event(kind string, message msg.Message) bool {
- log.Println(kind, "\t", message)
- if kind != "PART" && message.User.Name != p.Bot.Config().Nick {
- // user joined, let's nail them for it
- if kind == "NICK" {
- p.record(strings.ToLower(message.Channel))
- p.remove(strings.ToLower(message.User.Name))
- } else {
- p.record(strings.ToLower(message.User.Name))
- }
- } else if kind == "PART" || kind == "QUIT" {
- p.remove(strings.ToLower(message.User.Name))
- } else {
- log.Println("Unknown event: ", kind, message.User, message)
- p.record(strings.ToLower(message.User.Name))
- }
- return false
-}
-
-// Handler for bot's own messages
-func (p *DowntimePlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *DowntimePlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *DowntimePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/emojifyme/emojifyme.go b/plugins/emojifyme/emojifyme.go
index 424a88b..13ab96f 100644
--- a/plugins/emojifyme/emojifyme.go
+++ b/plugins/emojifyme/emojifyme.go
@@ -5,11 +5,12 @@ package emojifyme
import (
"encoding/json"
"io/ioutil"
- "log"
"math/rand"
"net/http"
"strings"
+ "github.com/rs/zerolog/log"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@@ -20,15 +21,15 @@ type EmojifyMePlugin struct {
Emoji map[string]string
}
-func New(bot bot.Bot) *EmojifyMePlugin {
+func New(b bot.Bot) *EmojifyMePlugin {
resp, err := http.Get("https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json")
if err != nil {
- log.Fatalf("Error generic emoji list: %s", err)
+ log.Fatal().Err(err).Msg("Error generic emoji list")
}
body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
- log.Fatalf("Error generic emoji list body: %s", err)
+ log.Fatal().Err(err).Msg("Error generic emoji list body")
}
type Emoji struct {
@@ -38,7 +39,7 @@ func New(bot bot.Bot) *EmojifyMePlugin {
var emoji []Emoji
err = json.Unmarshal(body, &emoji)
if err != nil {
- log.Fatalf("Error parsing emoji list: %s", err)
+ log.Fatal().Err(err).Msg("Error parsing emoji list")
}
emojiMap := map[string]string{}
@@ -48,14 +49,16 @@ func New(bot bot.Bot) *EmojifyMePlugin {
}
}
- return &EmojifyMePlugin{
- Bot: bot,
+ ep := &EmojifyMePlugin{
+ Bot: b,
GotBotEmoji: false,
Emoji: emojiMap,
}
+ b.Register(ep, bot.Message, ep.message)
+ return ep
}
-func (p *EmojifyMePlugin) Message(message msg.Message) bool {
+func (p *EmojifyMePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !p.GotBotEmoji {
p.GotBotEmoji = true
emojiMap := p.Bot.GetEmojiList()
@@ -64,61 +67,39 @@ func (p *EmojifyMePlugin) Message(message msg.Message) bool {
}
}
- inertTokens := p.Bot.Config().Emojify.Scoreless
+ inertTokens := p.Bot.Config().GetArray("Emojify.Scoreless", []string{})
emojied := 0.0
- tokens := strings.Fields(strings.ToLower(message.Body))
- for i, token := range tokens {
- if _, ok := p.Emoji[token]; ok {
- if !stringsContain(inertTokens, token) {
- emojied++
- }
- tokens[i] = ":" + token + ":"
- } else if strings.HasSuffix(token, "s") {
- //Check to see if we can strip the trailing "s" off and get an emoji
- temp := strings.TrimSuffix(token, "s")
- if _, ok := p.Emoji[temp]; ok {
- if !stringsContain(inertTokens, temp) {
- emojied++
- }
- tokens[i] = ":" + temp + ":s"
- } else if strings.HasSuffix(token, "es") {
- //Check to see if we can strip the trailing "es" off and get an emoji
- temp := strings.TrimSuffix(token, "es")
- if _, ok := p.Emoji[temp]; ok {
- if !stringsContain(inertTokens, temp) {
- emojied++
- }
- tokens[i] = ":" + temp + ":es"
+ emojys := []string{}
+ msg := strings.Replace(strings.ToLower(message.Body), "_", " ", -1)
+ for k, v := range p.Emoji {
+ k = strings.Replace(k, "_", " ", -1)
+ candidates := []string{
+ k,
+ k + "es",
+ k + "s",
+ }
+ for _, c := range candidates {
+ if strings.Contains(msg, " "+c+" ") ||
+ strings.HasPrefix(msg, c+" ") ||
+ strings.HasSuffix(msg, " "+c) ||
+ msg == c {
+ emojys = append(emojys, v)
+ if !stringsContain(inertTokens, k) && len(v) > 2 {
+ emojied += 1
}
}
}
}
- if emojied > 0 && rand.Float64() <= p.Bot.Config().Emojify.Chance*emojied {
- modified := strings.Join(tokens, " ")
- p.Bot.SendMessage(message.Channel, modified)
- return true
+
+ if emojied > 0 && rand.Float64() <= p.Bot.Config().GetFloat64("Emojify.Chance", 0.02)*emojied {
+ for _, e := range emojys {
+ p.Bot.Send(c, bot.Reaction, message.Channel, e, message)
+ }
+ return false
}
return false
}
-func (p *EmojifyMePlugin) Help(channel string, parts []string) {
-
-}
-
-func (p *EmojifyMePlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *EmojifyMePlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *EmojifyMePlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *EmojifyMePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
-
func stringsContain(haystack []string, needle string) bool {
for _, s := range haystack {
if s == needle {
diff --git a/plugins/fact/fact_test.go b/plugins/fact/fact_test.go
new file mode 100644
index 0000000..c774146
--- /dev/null
+++ b/plugins/fact/fact_test.go
@@ -0,0 +1,60 @@
+package fact
+
+import (
+ "github.com/velour/catbase/plugins/cli"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+)
+
+var c = &cli.CliPlugin{}
+
+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) (*FactoidPlugin, *bot.MockBot) {
+ mb := bot.NewMockBot()
+ f := New(mb) // for DB table
+ return f, mb
+}
+
+func TestReact(t *testing.T) {
+ msgs := []msg.Message{
+ makeMessage("user1", "!testing123 jesus"),
+ makeMessage("user2", "testing123"),
+ }
+ p, mb := makePlugin(t)
+
+ for _, m := range msgs {
+ p.message(c, bot.Message, m)
+ }
+ assert.Len(t, mb.Reactions, 1)
+ assert.Contains(t, mb.Reactions[0], "jesus")
+}
+
+func TestReactCantLearnSpaces(t *testing.T) {
+ msgs := []msg.Message{
+ makeMessage("user1", "!test jesus christ"),
+ }
+ p, mb := makePlugin(t)
+
+ for _, m := range msgs {
+ p.message(c, bot.Message, m)
+ }
+ assert.Len(t, mb.Messages, 1)
+ assert.Contains(t, mb.Messages[0], "not a valid")
+}
diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go
index 17a4e4a..0c60250 100644
--- a/plugins/fact/factoid.go
+++ b/plugins/fact/factoid.go
@@ -4,15 +4,17 @@ package fact
import (
"database/sql"
+ "encoding/json"
"fmt"
"html/template"
- "log"
"math/rand"
"net/http"
"regexp"
"strings"
"time"
+ "github.com/rs/zerolog/log"
+
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
@@ -22,14 +24,14 @@ import (
// respond to queries in a way that is unpredictable and fun
// factoid stores info about our factoid for lookup and later interaction
-type factoid struct {
- id sql.NullInt64
+type Factoid struct {
+ ID sql.NullInt64
Fact string
Tidbit string
Verb string
Owner string
- created time.Time
- accessed time.Time
+ Created time.Time
+ Accessed time.Time
Count int
}
@@ -38,14 +40,14 @@ type alias struct {
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
q := `select fact, next from factoid_alias where fact=?`
var next alias
err := db.Get(&next, q, a.Next)
if err != nil {
// 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 {
err := fmt.Errorf("Error resolvig alias %v: %v", a, err)
return nil, err
@@ -55,7 +57,7 @@ func (a *alias) resolve(db *sqlx.DB) (*factoid, error) {
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=?`
var a alias
err := db.Get(&a, q, fact)
@@ -89,9 +91,9 @@ func aliasFromStrings(from, to string) *alias {
return &alias{from, to}
}
-func (f *factoid) save(db *sqlx.DB) error {
+func (f *Factoid) Save(db *sqlx.DB) error {
var err error
- if f.id.Valid {
+ if f.ID.Valid {
// update
_, err = db.Exec(`update factoid set
fact=?,
@@ -105,12 +107,12 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit,
f.Verb,
f.Owner,
- f.accessed.Unix(),
+ f.Accessed.Unix(),
f.Count,
- f.id.Int64)
+ f.ID.Int64)
} else {
- f.created = time.Now()
- f.accessed = time.Now()
+ f.Created = time.Now()
+ f.Accessed = time.Now()
// insert
res, err := db.Exec(`insert into factoid (
fact,
@@ -125,8 +127,8 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit,
f.Verb,
f.Owner,
- f.created.Unix(),
- f.accessed.Unix(),
+ f.Created.Unix(),
+ f.Accessed.Unix(),
f.Count,
)
if err != nil {
@@ -134,23 +136,23 @@ func (f *factoid) save(db *sqlx.DB) error {
}
id, err := res.LastInsertId()
// hackhackhack?
- f.id.Int64 = id
- f.id.Valid = true
+ f.ID.Int64 = id
+ f.ID.Valid = true
}
return err
}
-func (f *factoid) delete(db *sqlx.DB) error {
+func (f *Factoid) delete(db *sqlx.DB) error {
var err error
- if f.id.Valid {
- _, err = db.Exec(`delete from factoid where id=?`, f.id)
+ if f.ID.Valid {
+ _, err = db.Exec(`delete from factoid where id=?`, f.ID)
}
- f.id.Valid = false
+ f.ID.Valid = false
return err
}
-func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
- var fs []*factoid
+func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*Factoid, error) {
+ var fs []*Factoid
query := `select
id,
fact,
@@ -166,15 +168,15 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
rows, err := db.Query(query,
"%"+fact+"%", "%"+tidbit+"%")
if err != nil {
- log.Printf("Error regexping for facts: %s", err)
+ log.Error().Err(err).Msg("Error regexping for facts")
return nil, err
}
for rows.Next() {
- var f factoid
+ var f Factoid
var tmpCreated int64
var tmpAccessed int64
err := rows.Scan(
- &f.id,
+ &f.ID,
&f.Fact,
&f.Tidbit,
&f.Verb,
@@ -186,15 +188,15 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
if err != nil {
return nil, err
}
- f.created = time.Unix(tmpCreated, 0)
- f.accessed = time.Unix(tmpAccessed, 0)
+ f.Created = time.Unix(tmpCreated, 0)
+ f.Accessed = time.Unix(tmpAccessed, 0)
fs = append(fs, &f)
}
return fs, err
}
-func getSingle(db *sqlx.DB) (*factoid, error) {
- var f factoid
+func GetSingle(db *sqlx.DB) (*Factoid, error) {
+ var f Factoid
var tmpCreated int64
var tmpAccessed int64
err := db.QueryRow(`select
@@ -208,7 +210,7 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
count
from factoid
order by random() limit 1;`).Scan(
- &f.id,
+ &f.ID,
&f.Fact,
&f.Tidbit,
&f.Verb,
@@ -217,13 +219,13 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
&tmpAccessed,
&f.Count,
)
- f.created = time.Unix(tmpCreated, 0)
- f.accessed = time.Unix(tmpAccessed, 0)
+ f.Created = time.Unix(tmpCreated, 0)
+ f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err
}
-func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
- var f factoid
+func GetSingleFact(db *sqlx.DB, fact string) (*Factoid, error) {
+ var f Factoid
var tmpCreated int64
var tmpAccessed int64
err := db.QueryRow(`select
@@ -239,7 +241,7 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
where fact like ?
order by random() limit 1;`,
fact).Scan(
- &f.id,
+ &f.ID,
&f.Fact,
&f.Tidbit,
&f.Verb,
@@ -248,22 +250,22 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
&tmpAccessed,
&f.Count,
)
- f.created = time.Unix(tmpCreated, 0)
- f.accessed = time.Unix(tmpAccessed, 0)
+ f.Created = time.Unix(tmpCreated, 0)
+ f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err
}
// Factoid provides the necessary plugin-wide needs
-type Factoid struct {
+type FactoidPlugin struct {
Bot bot.Bot
NotFound []string
- LastFact *factoid
+ LastFact *Factoid
db *sqlx.DB
}
// NewFactoid creates a new Factoid with the Plugin interface
-func New(botInst bot.Bot) *Factoid {
- p := &Factoid{
+func New(botInst bot.Bot) *FactoidPlugin {
+ p := &FactoidPlugin{
Bot: botInst,
NotFound: []string{
"I don't know.",
@@ -276,6 +278,8 @@ func New(botInst bot.Bot) *Factoid {
db: botInst.DB(),
}
+ c := botInst.DefaultConnector()
+
if _, err := p.db.Exec(`create table if not exists factoid (
id integer primary key,
fact string,
@@ -286,7 +290,7 @@ func New(botInst bot.Bot) *Factoid {
accessed integer,
count integer
);`); err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
if _, err := p.db.Exec(`create table if not exists factoid_alias (
@@ -294,17 +298,17 @@ func New(botInst bot.Bot) *Factoid {
next string,
primary key (fact, next)
);`); err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
- for _, channel := range botInst.Config().Channels {
- go p.factTimer(channel)
+ for _, channel := range botInst.Config().GetArray("channels", []string{}) {
+ go p.factTimer(c, channel)
go func(ch string) {
// Some random time to start up
time.Sleep(time.Duration(15) * time.Second)
- if ok, fact := p.findTrigger(p.Bot.Config().Factoid.StartupFact); ok {
- p.sayFact(msg.Message{
+ if ok, fact := p.findTrigger(p.Bot.Config().Get("Factoid.StartupFact", "speed test")); ok {
+ p.sayFact(c, msg.Message{
Channel: ch,
Body: "speed test", // BUG: This is defined in the config too
Command: true,
@@ -314,6 +318,11 @@ func New(botInst bot.Bot) *Factoid {
}(channel)
}
+ botInst.Register(p, bot.Message, p.message)
+ botInst.Register(p, bot.Help, p.help)
+
+ p.registerWeb()
+
return p
}
@@ -338,45 +347,53 @@ func findAction(message string) string {
// learnFact assumes we have a learning situation and inserts a new fact
// into the database
-func (p *Factoid) learnFact(message msg.Message, fact, verb, tidbit string) bool {
+func (p *FactoidPlugin) learnFact(message msg.Message, fact, verb, tidbit string) error {
verb = strings.ToLower(verb)
+ if verb == "react" {
+ // This would be a great place to check against the API for valid emojy
+ // I'm too lazy for that
+ tidbit = strings.Replace(tidbit, ":", "", -1)
+ if len(strings.Split(tidbit, " ")) > 1 {
+ return fmt.Errorf("That's not a valid emojy.")
+ }
+ }
var count sql.NullInt64
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
+ log.Error().Err(err).Msg("Error counting facts")
+ return fmt.Errorf("What?")
} else if count.Valid && count.Int64 != 0 {
- log.Println("User tried to relearn a fact.")
- return false
+ log.Debug().Msg("User tried to relearn a fact.")
+ return fmt.Errorf("Look, I already know that.")
}
- n := factoid{
+ n := Factoid{
Fact: fact,
Tidbit: tidbit,
Verb: verb,
Owner: message.User.Name,
- created: time.Now(),
- accessed: time.Now(),
+ Created: time.Now(),
+ Accessed: time.Now(),
Count: 0,
}
p.LastFact = &n
- err = n.save(p.db)
+ err = n.Save(p.db)
if err != nil {
- log.Println("Error inserting fact: ", err)
- return false
+ log.Error().Err(err).Msg("Error inserting fact")
+ return fmt.Errorf("My brain is overheating.")
}
- return true
+ return nil
}
// 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
- f, err := getSingleFact(p.db, fact)
+ f, err := GetSingleFact(p.db, fact)
if err != nil {
return findAlias(p.db, fact)
}
@@ -385,7 +402,7 @@ func (p *Factoid) findTrigger(fact string) (bool, *factoid) {
// sayFact spits out a fact to the channel and updates the fact in the database
// with new time and count information
-func (p *Factoid) sayFact(message msg.Message, fact factoid) {
+func (p *FactoidPlugin) sayFact(c bot.Connector, message msg.Message, fact Factoid) {
msg := p.Bot.Filter(message, fact.Tidbit)
full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s",
fact.Fact, fact.Verb, fact.Tidbit,
@@ -397,39 +414,42 @@ func (p *Factoid) sayFact(message msg.Message, fact factoid) {
}
if fact.Verb == "action" {
- p.Bot.SendAction(message.Channel, msg)
+ p.Bot.Send(c, bot.Action, message.Channel, msg)
+ } else if fact.Verb == "react" {
+ p.Bot.Send(c, bot.Reaction, message.Channel, msg, message)
} else if fact.Verb == "reply" {
- p.Bot.SendMessage(message.Channel, msg)
+ p.Bot.Send(c, bot.Message, message.Channel, msg)
} else {
- p.Bot.SendMessage(message.Channel, full)
+ p.Bot.Send(c, bot.Message, message.Channel, full)
}
}
// update fact tracking
- fact.accessed = time.Now()
+ fact.Accessed = time.Now()
fact.Count += 1
- err := fact.save(p.db)
+ err := fact.Save(p.db)
if err != nil {
- log.Printf("Could not update fact.\n")
- log.Printf("%#v\n", fact)
- log.Println(err)
+ log.Error().
+ Interface("fact", fact).
+ Err(err).
+ Msg("could not update fact")
}
p.LastFact = &fact
}
// 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
-func (p *Factoid) trigger(message msg.Message) bool {
- minLen := p.Bot.Config().Factoid.MinLen
+func (p *FactoidPlugin) trigger(c bot.Connector, message msg.Message) bool {
+ minLen := p.Bot.Config().GetInt("Factoid.MinLen", 4)
if len(message.Body) > minLen || message.Command || message.Body == "..." {
if ok, fact := p.findTrigger(message.Body); ok {
- p.sayFact(message, *fact)
+ p.sayFact(c, message, *fact)
return true
}
r := strings.NewReplacer("'", "", "\"", "", ",", "", ".", "", ":", "",
"?", "", "!", "")
if ok, fact := p.findTrigger(r.Replace(message.Body)); ok {
- p.sayFact(message, *fact)
+ p.sayFact(c, message, *fact)
return true
}
}
@@ -438,20 +458,20 @@ func (p *Factoid) trigger(message msg.Message) bool {
}
// tellThemWhatThatWas is a hilarious name for a function.
-func (p *Factoid) tellThemWhatThatWas(message msg.Message) bool {
+func (p *FactoidPlugin) tellThemWhatThatWas(c bot.Connector, message msg.Message) bool {
fact := p.LastFact
var msg string
if fact == nil {
msg = "Nope."
} else {
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.SendMessage(message.Channel, msg)
+ p.Bot.Send(c, bot.Message, message.Channel, msg)
return true
}
-func (p *Factoid) learnAction(message msg.Message, action string) bool {
+func (p *FactoidPlugin) learnAction(c bot.Connector, message msg.Message, action string) bool {
body := message.Body
parts := strings.SplitN(body, action, 2)
@@ -465,21 +485,21 @@ func (p *Factoid) learnAction(message msg.Message, action string) bool {
action = strings.TrimSpace(action)
if len(trigger) == 0 || len(fact) == 0 || len(action) == 0 {
- p.Bot.SendMessage(message.Channel, "I don't want to learn that.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I don't want to learn that.")
return true
}
if len(strings.Split(fact, "$and")) > 4 {
- p.Bot.SendMessage(message.Channel, "You can't use more than 4 $and operators.")
+ p.Bot.Send(c, bot.Message, message.Channel, "You can't use more than 4 $and operators.")
return true
}
strippedaction := strings.Replace(strings.Replace(action, "<", "", 1), ">", "", 1)
- if p.learnFact(message, trigger, strippedaction, fact) {
- p.Bot.SendMessage(message.Channel, fmt.Sprintf("Okay, %s.", message.User.Name))
+ if err := p.learnFact(message, trigger, strippedaction, fact); err != nil {
+ p.Bot.Send(c, bot.Message, message.Channel, err.Error())
} else {
- p.Bot.SendMessage(message.Channel, "I already know that.")
+ p.Bot.Send(c, bot.Message, message.Channel, fmt.Sprintf("Okay, %s.", message.User.Name))
}
return true
@@ -497,26 +517,29 @@ func changeOperator(body string) string {
// If the user requesting forget is either the owner of the last learned fact or
// an admin, it may be deleted
-func (p *Factoid) forgetLastFact(message msg.Message) bool {
+func (p *FactoidPlugin) forgetLastFact(c bot.Connector, message msg.Message) bool {
if p.LastFact == nil {
- p.Bot.SendMessage(message.Channel, "I refuse.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I refuse.")
return true
}
err := p.LastFact.delete(p.db)
if err != nil {
- log.Println("Error removing fact: ", p.LastFact, err)
+ log.Error().
+ Err(err).
+ Interface("LastFact", p.LastFact).
+ 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.Int64, p.LastFact.Fact,
p.LastFact.Verb, p.LastFact.Tidbit)
- p.Bot.SendAction(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
return true
}
// Allow users to change facts with a simple regexp
-func (p *Factoid) changeFact(message msg.Message) bool {
+func (p *FactoidPlugin) changeFact(c bot.Connector, message msg.Message) bool {
oper := changeOperator(message.Body)
parts := strings.SplitN(message.Body, oper, 2)
userexp := strings.TrimSpace(parts[1])
@@ -524,12 +547,16 @@ func (p *Factoid) changeFact(message msg.Message) bool {
parts = strings.Split(userexp, "/")
- log.Printf("changeFact: %s %s %#v", trigger, userexp, parts)
+ log.Debug().
+ Str("trigger", trigger).
+ Str("userexp", userexp).
+ Strs("parts", parts).
+ Msg("changefact")
if len(parts) == 4 {
// replacement
if parts[0] != "s" {
- p.Bot.SendMessage(message.Channel, "Nah.")
+ p.Bot.Send(c, bot.Message, message.Channel, "Nah.")
}
find := parts[1]
replace := parts[2]
@@ -537,17 +564,20 @@ func (p *Factoid) changeFact(message msg.Message) bool {
// replacement
result, err := getFacts(p.db, trigger, parts[1])
if err != nil {
- log.Println("Error getting facts: ", trigger, err)
+ log.Error().
+ Err(err).
+ Str("trigger", trigger).
+ Msg("Error getting facts")
}
if userexp[len(userexp)-1] != 'g' {
result = result[:1]
}
// make the changes
msg := fmt.Sprintf("Changing %d facts.", len(result))
- p.Bot.SendMessage(message.Channel, msg)
+ p.Bot.Send(c, bot.Message, message.Channel, msg)
reg, err := regexp.Compile(find)
if err != nil {
- p.Bot.SendMessage(message.Channel, "I don't really want to.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I don't really want to.")
return false
}
for _, fact := range result {
@@ -556,27 +586,30 @@ func (p *Factoid) changeFact(message msg.Message) bool {
fact.Verb = reg.ReplaceAllString(fact.Verb, replace)
fact.Tidbit = reg.ReplaceAllString(fact.Tidbit, replace)
fact.Count += 1
- fact.accessed = time.Now()
- fact.save(p.db)
+ fact.Accessed = time.Now()
+ fact.Save(p.db)
}
} else if len(parts) == 3 {
// search for a factoid and print it
result, err := getFacts(p.db, trigger, parts[1])
if err != nil {
- log.Println("Error getting facts: ", trigger, err)
- p.Bot.SendMessage(message.Channel, "bzzzt")
+ log.Error().
+ Err(err).
+ Str("trigger", trigger).
+ Msg("Error getting facts")
+ p.Bot.Send(c, bot.Message, message.Channel, "bzzzt")
return true
}
count := len(result)
if count == 0 {
- p.Bot.SendMessage(message.Channel, "I didn't find any facts like that.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I didn't find any facts like that.")
return true
}
if parts[2] == "g" && len(result) > 4 {
// summarize
result = result[:4]
} else {
- p.sayFact(message, *result[0])
+ p.sayFact(c, message, *result[0])
return true
}
msg := fmt.Sprintf("%s ", trigger)
@@ -589,9 +622,9 @@ func (p *Factoid) changeFact(message msg.Message) bool {
if count > 4 {
msg = fmt.Sprintf("%s | ...and %d others", msg, count)
}
- p.Bot.SendMessage(message.Channel, msg)
+ p.Bot.Send(c, bot.Message, message.Channel, msg)
} else {
- p.Bot.SendMessage(message.Channel, "I don't know what you mean.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I don't know what you mean.")
}
return true
}
@@ -599,79 +632,77 @@ func (p *Factoid) changeFact(message msg.Message) bool {
// 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 *Factoid) Message(message msg.Message) bool {
+func (p *FactoidPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "what was that?" {
- return p.tellThemWhatThatWas(message)
+ return p.tellThemWhatThatWas(c, message)
}
// This plugin has no business with normal messages
if !message.Command {
// look for any triggers in the db matching this message
- return p.trigger(message)
+ return p.trigger(c, message)
}
if strings.HasPrefix(strings.ToLower(message.Body), "alias") {
- log.Printf("Trying to learn an alias: %s", message.Body)
+ log.Debug().
+ Str("alias", message.Body).
+ Msg("Trying to learn an alias")
m := strings.TrimPrefix(message.Body, "alias ")
parts := strings.SplitN(m, "->", 2)
if len(parts) != 2 {
- p.Bot.SendMessage(message.Channel, "If you want to alias something, use: `alias this -> that`")
+ p.Bot.Send(c, bot.Message, message.Channel, "If you want to alias something, use: `alias this -> that`")
return true
}
a := aliasFromStrings(strings.TrimSpace(parts[1]), strings.TrimSpace(parts[0]))
if err := a.save(p.db); err != nil {
- p.Bot.SendMessage(message.Channel, err.Error())
+ p.Bot.Send(c, bot.Message, message.Channel, err.Error())
} else {
- p.Bot.SendAction(message.Channel, "learns a new synonym")
+ p.Bot.Send(c, bot.Action, message.Channel, "learns a new synonym")
}
return true
}
if strings.ToLower(message.Body) == "factoid" {
if fact := p.randomFact(); fact != nil {
- p.sayFact(message, *fact)
+ p.sayFact(c, message, *fact)
return true
}
- log.Println("Got a nil fact.")
+ log.Debug().Msg("Got a nil fact.")
}
if strings.ToLower(message.Body) == "forget that" {
- return p.forgetLastFact(message)
+ return p.forgetLastFact(c, message)
}
if changeOperator(message.Body) != "" {
- return p.changeFact(message)
+ return p.changeFact(c, message)
}
action := findAction(message.Body)
if action != "" {
- return p.learnAction(message, action)
+ return p.learnAction(c, message, action)
}
// look for any triggers in the db matching this message
- if p.trigger(message) {
+ if p.trigger(c, message) {
return true
}
// We didn't find anything, panic!
- p.Bot.SendMessage(message.Channel, p.NotFound[rand.Intn(len(p.NotFound))])
+ p.Bot.Send(c, bot.Message, message.Channel, p.NotFound[rand.Intn(len(p.NotFound))])
return true
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *Factoid) 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 $5\". Later, trigger the factoid by just saying the trigger word, \"this\" or \"he\" in these examples.")
- p.Bot.SendMessage(channel, "I can also figure out some variables including: $nonzero, $digit, $nick, and $someone.")
-}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *Factoid) Event(kind string, message msg.Message) bool {
- return false
+func (p *FactoidPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.Bot.Send(c, bot.Message, message.Channel, "I can learn facts and spit them back out. You can say \"this is that\" or \"he $5\". Later, trigger the factoid by just saying the trigger word, \"this\" or \"he\" in these examples.")
+ p.Bot.Send(c, bot.Message, message.Channel, "I can also figure out some variables including: $nonzero, $digit, $nick, and $someone.")
+ return true
}
// Pull a fact at random from the database
-func (p *Factoid) randomFact() *factoid {
- f, err := getSingle(p.db)
+func (p *FactoidPlugin) randomFact() *Factoid {
+ f, err := GetSingle(p.db)
if err != nil {
fmt.Println("Error getting a fact: ", err)
return nil
@@ -680,8 +711,13 @@ func (p *Factoid) randomFact() *factoid {
}
// factTimer spits out a fact at a given interval and with given probability
-func (p *Factoid) factTimer(channel string) {
- duration := time.Duration(p.Bot.Config().Factoid.QuoteTime) * time.Minute
+func (p *FactoidPlugin) factTimer(c bot.Connector, channel string) {
+ quoteTime := p.Bot.Config().GetInt("Factoid.QuoteTime", 30)
+ if quoteTime == 0 {
+ quoteTime = 30
+ p.Bot.Config().Set("Factoid.QuoteTime", "30")
+ }
+ duration := time.Duration(quoteTime) * time.Minute
myLastMsg := time.Now()
for {
time.Sleep(time.Duration(5) * time.Second) // why 5?
@@ -695,12 +731,17 @@ func (p *Factoid) factTimer(channel string) {
tdelta := time.Since(lastmsg.Time)
earlier := time.Since(myLastMsg) > tdelta
chance := rand.Float64()
- success := chance < p.Bot.Config().Factoid.QuoteChance
+ quoteChance := p.Bot.Config().GetFloat64("Factoid.QuoteChance", 0.99)
+ if quoteChance == 0.0 {
+ quoteChance = 0.99
+ p.Bot.Config().Set("Factoid.QuoteChance", "0.99")
+ }
+ success := chance < quoteChance
if success && tdelta > duration && earlier {
fact := p.randomFact()
if fact == nil {
- log.Println("Didn't find a random fact to say")
+ log.Debug().Msg("Didn't find a random fact to say")
continue
}
@@ -711,23 +752,18 @@ func (p *Factoid) factTimer(channel string) {
User: &users[rand.Intn(len(users))],
Channel: channel,
}
- p.sayFact(message, *fact)
+ p.sayFact(c, message, *fact)
myLastMsg = time.Now()
}
}
}
-// Handler for bot's own messages
-func (p *Factoid) BotMessage(message msg.Message) bool {
- return false
-}
-
// Register any web URLs desired
-func (p *Factoid) RegisterWeb() *string {
+func (p *FactoidPlugin) registerWeb() {
+ http.HandleFunc("/factoid/api", p.serveAPI)
http.HandleFunc("/factoid/req", p.serveQuery)
http.HandleFunc("/factoid", p.serveQuery)
- tmp := "/factoid"
- return &tmp
+ p.Bot.RegisterWeb("/factoid", "Factoid")
}
func linkify(text string) template.HTML {
@@ -739,30 +775,40 @@ func linkify(text string) template.HTML {
}
return template.HTML(strings.Join(parts, " "))
}
+func (p *FactoidPlugin) serveAPI(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ fmt.Fprintf(w, "Incorrect HTTP method")
+ return
+ }
+ info := struct {
+ Query string `json:"query"`
+ }{}
+ decoder := json.NewDecoder(r.Body)
+ err := decoder.Decode(&info)
+ if err != nil {
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
+ }
-func (p *Factoid) serveQuery(w http.ResponseWriter, r *http.Request) {
- context := make(map[string]interface{})
- funcMap := template.FuncMap{
- // The name "title" is what the function will be called in the template text.
- "linkify": linkify,
- }
- if e := r.FormValue("entry"); e != "" {
- entries, err := getFacts(p.db, e, "")
- if err != nil {
- log.Println("Web error searching: ", err)
- }
- context["Count"] = fmt.Sprintf("%d", len(entries))
- context["Entries"] = entries
- context["Search"] = e
- }
- t, err := template.New("factoidIndex").Funcs(funcMap).Parse(factoidIndex)
+ entries, err := getFacts(p.db, info.Query, "")
if err != nil {
- log.Println(err)
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
}
- err = t.Execute(w, context)
+
+ data, err := json.Marshal(entries)
if err != nil {
- log.Println(err)
+ w.WriteHeader(500)
+ fmt.Fprint(w, err)
+ return
}
+ w.Write(data)
}
-func (p *Factoid) ReplyMessage(message msg.Message, identifier string) bool { return false }
+var tpl = template.Must(template.New("factoidIndex").Parse(factoidIndex))
+
+func (p *FactoidPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
+ tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.Bot.GetWebNavigation()})
+}
diff --git a/plugins/fact/remember.go b/plugins/fact/remember.go
deleted file mode 100644
index 1038eb7..0000000
--- a/plugins/fact/remember.go
+++ /dev/null
@@ -1,174 +0,0 @@
-// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
-
-package fact
-
-import (
- "fmt"
- "log"
- "strings"
- "time"
-
- "github.com/jmoiron/sqlx"
- "github.com/velour/catbase/bot"
- "github.com/velour/catbase/bot/msg"
-)
-
-// This is a skeleton plugin to serve as an example and quick copy/paste for new
-// plugins.
-
-type RememberPlugin struct {
- Bot bot.Bot
- Log map[string][]msg.Message
- db *sqlx.DB
-}
-
-// NewRememberPlugin creates a new RememberPlugin with the Plugin interface
-func NewRemember(b bot.Bot) *RememberPlugin {
- p := RememberPlugin{
- Bot: b,
- Log: make(map[string][]msg.Message),
- db: b.DB(),
- }
- 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(message msg.Message) bool {
-
- if strings.ToLower(message.Body) == "quote" && message.Command {
- q := p.randQuote()
- p.Bot.SendMessage(message.Channel, q)
-
- // is it evil not to remember that the user said quote?
- return true
- }
-
- user := message.User
- parts := strings.Fields(message.Body)
- if message.Command && len(parts) >= 3 &&
- strings.ToLower(parts[0]) == "remember" {
-
- // we have a remember!
- // look through the logs and find parts[1] as a user, if not,
- // fuck this hoser
- nick := parts[1]
- snip := strings.Join(parts[2:], " ")
- for i := len(p.Log[message.Channel]) - 1; i >= 0; i-- {
- entry := p.Log[message.Channel][i]
- log.Printf("Comparing %s:%s with %s:%s",
- entry.User.Name, entry.Body, nick, snip)
- if strings.ToLower(entry.User.Name) == strings.ToLower(nick) &&
- strings.Contains(
- strings.ToLower(entry.Body),
- strings.ToLower(snip),
- ) {
- log.Printf("Found!")
-
- var msg string
- if entry.Action {
- msg = fmt.Sprintf("*%s* %s", entry.User.Name, entry.Body)
- } else {
- msg = fmt.Sprintf("<%s> %s", entry.User.Name, entry.Body)
- }
-
- trigger := fmt.Sprintf("%s quotes", entry.User.Name)
-
- fact := factoid{
- Fact: strings.ToLower(trigger),
- Verb: "reply",
- Tidbit: msg,
- Owner: user.Name,
- created: time.Now(),
- accessed: time.Now(),
- Count: 0,
- }
- if err := fact.save(p.db); err != nil {
- log.Println("ERROR!!!!:", err)
- p.Bot.SendMessage(message.Channel, "Tell somebody I'm broke.")
- }
-
- log.Println("Remembering factoid:", msg)
-
- // sorry, not creative with names so we're reusing msg
- msg = fmt.Sprintf("Okay, %s, remembering '%s'.",
- message.User.Name, msg)
- p.Bot.SendMessage(message.Channel, msg)
- p.recordMsg(message)
- return true
-
- }
- }
-
- p.Bot.SendMessage(message.Channel, "Sorry, I don't know that phrase.")
- p.recordMsg(message)
- return true
- }
- p.recordMsg(message)
- return false
-}
-
-// Help responds to help requests. Every plugin must implement a help function.
-func (p *RememberPlugin) Help(channel string, parts []string) {
-
- msg := "!remember will let you quote your idiot friends. Just type " +
- "!remember to remember what they said. Snippet can " +
- "be any part of their message. Later on, you can ask for a random " +
- "!quote."
-
- p.Bot.SendMessage(channel, msg)
-}
-
-// deliver a random quote out of the db.
-// 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
-func (p *RememberPlugin) randQuote() string {
-
- var f factoid
- 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 {
- log.Println("Error getting quotes: ", err)
- return "I had a problem getting your quote."
- }
- f.created = time.Unix(tmpCreated, 0)
- f.accessed = time.Unix(tmpAccessed, 0)
-
- return f.Tidbit
-}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *RememberPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Record what the bot says in the log
-func (p *RememberPlugin) BotMessage(message msg.Message) bool {
- p.recordMsg(message)
- return false
-}
-
-// Register any web URLs desired
-func (p *RememberPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *RememberPlugin) recordMsg(message msg.Message) {
- log.Printf("Logging message: %s: %s", message.User.Name, message.Body)
- p.Log[message.Channel] = append(p.Log[message.Channel], message)
-}
-
-func (p *RememberPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/fact/webTemplates.go b/plugins/fact/webTemplates.go
index d3f7ee6..3027235 100644
--- a/plugins/fact/webTemplates.go
+++ b/plugins/fact/webTemplates.go
@@ -7,106 +7,109 @@ package fact
// 2016-01-15 Later note, why are these in plugins and the server is in bot?
-var factoidIndex string = `
+var factoidIndex = `
-
+
- Factoids
-
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Factoids
-
-
-
+
-
-
-
- {{if .Error}}
- {{.Error}}
- {{end}}
-
- {{if .Count}}
- Found {{.Count}} entries.
- {{end}}
-
-
- {{if .Entries}}
-
-
-
-
- Trigger
- Full Text
- Author
- # Hits
-
-
-
-
- {{range .Entries}}
-
- {{linkify .Fact}}
- {{linkify .Tidbit}}
- {{linkify .Owner}}
- {{.Count}}
-
- {{end}}
-
-
-
- {{end}}
-
-
+
+
+ Factoids
+
+ {{ "{{ item.Name }}" }}
+
+
+
+ {{ "{{ err }}" }}
+
+
+
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
`
diff --git a/plugins/first/first.go b/plugins/first/first.go
index d7213f2..d078926 100644
--- a/plugins/first/first.go
+++ b/plugins/first/first.go
@@ -5,12 +5,12 @@ package first
import (
"database/sql"
"fmt"
- "log"
"regexp"
"strings"
"time"
"github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@@ -18,26 +18,27 @@ import (
// This is a first plugin to serve as an example and quick copy/paste for new plugins.
type FirstPlugin struct {
- First *FirstEntry
- Bot bot.Bot
- db *sqlx.DB
+ Bot bot.Bot
+ db *sqlx.DB
}
type FirstEntry struct {
- id int64
- day time.Time
- time time.Time
- body string
- nick string
- saved bool
+ id int64
+ day time.Time
+ time time.Time
+ channel string
+ body string
+ nick string
+ saved bool
}
// Insert or update the first entry
func (fe *FirstEntry) save(db *sqlx.DB) error {
- if _, err := db.Exec(`insert into first (day, time, body, nick)
- values (?, ?, ?, ?)`,
+ if _, err := db.Exec(`insert into first (day, time, channel, body, nick)
+ values (?, ?, ?, ?, ?)`,
fe.day.Unix(),
fe.time.Unix(),
+ fe.channel,
fe.body,
fe.nick,
); err != nil {
@@ -48,34 +49,33 @@ func (fe *FirstEntry) save(db *sqlx.DB) error {
// NewFirstPlugin creates a new FirstPlugin with the Plugin interface
func New(b bot.Bot) *FirstPlugin {
- if b.DBVersion() == 1 {
- _, err := b.DB().Exec(`create table if not exists first (
+ _, 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("Could not create first table: ", err)
- }
- }
-
- log.Println("First plugin initialized with day:", midnight(time.Now()))
-
- first, err := getLastFirst(b.DB())
if err != nil {
- log.Fatal("Could not initialize first plugin: ", err)
+ log.Fatal().
+ Err(err).
+ Msg("Could not create first table")
}
- return &FirstPlugin{
- Bot: b,
- db: b.DB(),
- First: first,
+ log.Info().Msgf("First plugin initialized with day: %s",
+ midnight(time.Now()))
+
+ fp := &FirstPlugin{
+ Bot: b,
+ db: b.DB(),
}
+ b.Register(fp, bot.Message, fp.message)
+ b.Register(fp, bot.Help, fp.help)
+ return fp
}
-func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
+func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) {
// Get last first entry
var id sql.NullInt64
var day sql.NullInt64
@@ -85,8 +85,9 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
err := db.QueryRow(`select
id, max(day), time, body, nick from first
+ where channel = ?
limit 1;
- `).Scan(
+ `, channel).Scan(
&id,
&day,
&timeEntered,
@@ -95,20 +96,22 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
)
switch {
case err == sql.ErrNoRows || !id.Valid:
- log.Println("No previous first entries")
+ log.Info().Msg("No previous first entries")
return nil, nil
case err != nil:
- log.Println("Error on first query row: ", err)
+ log.Warn().Err(err).Msg("Error on first query row")
return nil, err
}
- log.Println(id, day, timeEntered, body, nick)
+ log.Debug().Msgf("id: %v day %v time %v body %v nick %v",
+ id, day, timeEntered, body, nick)
return &FirstEntry{
- id: id.Int64,
- day: time.Unix(day.Int64, 0),
- time: time.Unix(timeEntered.Int64, 0),
- body: body.String,
- nick: nick.String,
- saved: true,
+ 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
}
@@ -117,7 +120,11 @@ func midnight(t time.Time) time.Time {
return time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
}
-func isToday(t time.Time) bool {
+func isNotToday(f *FirstEntry) bool {
+ if f == nil {
+ return true
+ }
+ t := f.time
t0 := midnight(t)
return t0.Before(midnight(time.Now()))
}
@@ -125,26 +132,42 @@ func isToday(t time.Time) bool {
// 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 *FirstPlugin) Message(message msg.Message) bool {
- // This bot does not reply to anything
+func (p *FirstPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ log.Debug().
+ Interface("msg", message).
+ Msg("First is looking at a message")
- if p.First == nil && p.allowed(message) {
- log.Printf("No previous first. Recording new first: %s", message.Body)
- p.recordFirst(message)
+ if message.IsIM {
+ log.Debug().Msg("Skipping IM")
return false
- } else if p.First != nil {
- if isToday(p.First.time) && p.allowed(message) {
- log.Printf("Recording first: %s - %v vs %v", message.Body, p.First.time, time.Now())
- p.recordFirst(message)
- return false
- }
}
- r := strings.NewReplacer("'", "", "\"", "", ",", "", ".", "", ":", "",
+ first, err := getLastFirst(p.db, message.Channel)
+ if err != nil {
+ log.Error().
+ Err(err).
+ Msg("Error getting last first")
+ }
+
+ log.Debug().Bool("first == nil", first == nil).Msg("Is first nil?")
+ log.Debug().Bool("first == nil || isNotToday()", isNotToday(first)).Msg("Is it today?")
+ log.Debug().Bool("p.allowed", p.allowed(message)).Msg("Allowed?")
+
+ if (first == nil || isNotToday(first)) && p.allowed(message) {
+ log.Debug().
+ Str("body", message.Body).
+ Interface("t0", first).
+ Time("t1", time.Now()).
+ Msg("Recording first")
+ p.recordFirst(c, message)
+ return false
+ }
+
+ r := strings.NewReplacer("’", "", "'", "", "\"", "", ",", "", ".", "", ":", "",
"?", "", "!", "")
- msg := strings.ToLower(message.Body)
- if r.Replace(msg) == "whos on first" {
- p.announceFirst(message)
+ m := strings.ToLower(message.Body)
+ if r.Replace(m) == "whos on first" && first != nil {
+ p.announceFirst(c, first)
return true
}
@@ -152,81 +175,70 @@ func (p *FirstPlugin) Message(message msg.Message) bool {
}
func (p *FirstPlugin) allowed(message msg.Message) bool {
- for _, msg := range p.Bot.Config().Bad.Msgs {
- match, err := regexp.MatchString(msg, strings.ToLower(message.Body))
+ for _, m := range p.Bot.Config().GetArray("Bad.Msgs", []string{}) {
+ match, err := regexp.MatchString(m, strings.ToLower(message.Body))
if err != nil {
- log.Println("Bad regexp: ", err)
+ log.Error().Err(err).Msg("Bad regexp")
}
if match {
- log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
+ log.Info().
+ Str("user", message.User.Name).
+ Str("body", message.Body).
+ Msg("Disallowing first")
return false
}
}
- for _, host := range p.Bot.Config().Bad.Hosts {
+ for _, host := range p.Bot.Config().GetArray("Bad.Hosts", []string{}) {
if host == message.Host {
- log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
+ log.Info().
+ Str("user", message.User.Name).
+ Str("body", message.Body).
+ Msg("Disallowing first")
return false
}
}
- for _, nick := range p.Bot.Config().Bad.Nicks {
+ for _, nick := range p.Bot.Config().GetArray("Bad.Nicks", []string{}) {
if nick == message.User.Name {
- log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
+ log.Info().
+ Str("user", message.User.Name).
+ Str("body", message.Body).
+ Msg("Disallowing first")
return false
}
}
return true
}
-func (p *FirstPlugin) recordFirst(message msg.Message) {
- log.Println("Recording first: ", message.User.Name, ":", message.Body)
- p.First = &FirstEntry{
- day: midnight(time.Now()),
- time: message.Time,
- body: message.Body,
- nick: message.User.Name,
+func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) {
+ log.Info().
+ Str("channel", message.Channel).
+ Str("user", message.User.Name).
+ Str("body", message.Body).
+ Msg("Recording first")
+ first := &FirstEntry{
+ day: midnight(time.Now()),
+ time: message.Time,
+ channel: message.Channel,
+ body: message.Body,
+ nick: message.User.Name,
}
- log.Printf("recordFirst: %+v", p.First.day)
- err := p.First.save(p.db)
+ log.Info().Msgf("recordFirst: %+v", first.day)
+ err := first.save(p.db)
if err != nil {
- log.Println("Error saving first entry: ", err)
+ log.Error().Err(err).Msg("Error saving first entry")
return
}
- p.announceFirst(message)
+ p.announceFirst(c, first)
}
-func (p *FirstPlugin) announceFirst(message msg.Message) {
- c := message.Channel
- if p.First != nil {
- p.Bot.SendMessage(c, fmt.Sprintf("%s had first at %s with the message: \"%s\"",
- p.First.nick, p.First.time.Format("15:04"), p.First.body))
- }
-}
-
-// 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 *FirstPlugin) LoadData() {
- // This bot has no data to load
+func (p *FirstPlugin) announceFirst(c bot.Connector, first *FirstEntry) {
+ ch := first.channel
+ p.Bot.Send(c, bot.Message, ch, fmt.Sprintf("%s had first at %s with the message: \"%s\"",
+ first.nick, first.time.Format("15:04"), first.body))
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *FirstPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Sorry, First does not do a goddamn thing.")
+func (p *FirstPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.Bot.Send(c, bot.Message, message.Channel, "You can ask 'who's on first?' to find out.")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *FirstPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *FirstPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *FirstPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *FirstPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/inventory/inventory.go b/plugins/inventory/inventory.go
index 7efb807..2eb4a2f 100644
--- a/plugins/inventory/inventory.go
+++ b/plugins/inventory/inventory.go
@@ -6,10 +6,11 @@ package inventory
import (
"fmt"
- "log"
"regexp"
"strings"
+ "github.com/rs/zerolog/log"
+
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
@@ -24,38 +25,41 @@ type InventoryPlugin struct {
}
// New creates a new InventoryPlugin with the Plugin interface
-func New(bot bot.Bot) *InventoryPlugin {
- config := bot.Config()
+func New(b bot.Bot) *InventoryPlugin {
+ config := b.Config()
+ nick := config.Get("nick", "bot")
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))
+ r3, err := regexp.Compile(fmt.Sprintf("puts (.+) in %s([^a-zA-Z].*)?", nick))
checkerr(err)
- r4, err := regexp.Compile(fmt.Sprintf("gives %s (.+)", config.Nick))
+ r4, err := regexp.Compile(fmt.Sprintf("gives %s (.+)", nick))
checkerr(err)
- r5, err := regexp.Compile(fmt.Sprintf("gives (.+) to %s([^a-zA-Z].*)?", config.Nick))
+ r5, err := regexp.Compile(fmt.Sprintf("gives (.+) to %s([^a-zA-Z].*)?", nick))
checkerr(err)
- p := InventoryPlugin{
- DB: bot.DB(),
- bot: bot,
+ p := &InventoryPlugin{
+ DB: b.DB(),
+ bot: b,
config: config,
r1: r1, r2: r2, r3: r3, r4: r4, r5: r5,
}
- bot.RegisterFilter("$item", p.itemFilter)
- bot.RegisterFilter("$giveitem", p.giveItemFilter)
+ b.RegisterFilter("$item", p.itemFilter)
+ b.RegisterFilter("$giveitem", p.giveItemFilter)
_, err = p.DB.Exec(`create table if not exists inventory (
item string primary key
);`)
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
- return &p
+ b.Register(p, bot.Message, p.message)
+
+ return p
}
func (p *InventoryPlugin) giveItemFilter(input string) string {
@@ -74,49 +78,49 @@ func (p *InventoryPlugin) itemFilter(input string) string {
return input
}
-func (p *InventoryPlugin) Message(message msg.Message) bool {
+func (p *InventoryPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
m := message.Body
- log.Printf("inventory trying to read %+v", message)
+ log.Debug().Msgf("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))
+ log.Debug().Msgf("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)
+ p.bot.Send(c, bot.Message, 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])
+ log.Debug().Msgf("Found item to add: %s", matches[1])
+ return p.addItem(c, 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])
+ log.Debug().Msgf("Found item to add: %s", matches[1])
+ return p.addItem(c, message, matches[1])
}
}
if message.Action {
- log.Println("Inventory found an action")
+ log.Debug().Msg("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])
+ log.Debug().Msgf("Found item to add: %s", matches[1])
+ return p.addItem(c, 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])
+ log.Debug().Msgf("Found item to add: %s", matches[1])
+ return p.addItem(c, 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])
+ log.Debug().Msgf("Found item to add: %s", matches[1])
+ return p.addItem(c, message, matches[1])
}
}
return false
@@ -128,12 +132,12 @@ func (p *InventoryPlugin) removeRandom() string {
&name,
)
if err != nil {
- log.Printf("Error finding random entry: %s", err)
+ log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR"
}
_, err = p.Exec(`delete from inventory where item=?`, name)
if err != nil {
- log.Printf("Error finding random entry: %s", err)
+ log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR"
}
return name
@@ -143,7 +147,7 @@ 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)
+ log.Error().Err(err).Msg("Error checking for item")
return -1
}
return output
@@ -155,7 +159,7 @@ func (p *InventoryPlugin) random() string {
&name,
)
if err != nil {
- log.Printf("Error finding random entry: %s", err)
+ log.Error().Err(err).Msg("Error finding random entry")
return "IAMERROR"
}
return name
@@ -164,7 +168,7 @@ func (p *InventoryPlugin) random() string {
func (p *InventoryPlugin) getAll() []string {
rows, err := p.Queryx(`select item from inventory`)
if err != nil {
- log.Printf("Error getting all items: %s", err)
+ log.Error().Err(err).Msg("Error getting all items")
return []string{}
}
output := []string{}
@@ -181,7 +185,7 @@ 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)
+ log.Error().Err(err).Msg("Error checking for item")
return false
}
return output > 0
@@ -190,51 +194,34 @@ func (p *InventoryPlugin) exists(i string) bool {
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)
+ log.Error().Msg("Error inserting new inventory item")
}
}
-func (p *InventoryPlugin) addItem(m msg.Message, i string) bool {
+func (p *InventoryPlugin) addItem(c bot.Connector, m msg.Message, i string) bool {
if p.exists(i) {
- p.bot.SendMessage(m.Channel, fmt.Sprintf("I already have %s.", i))
+ p.bot.Send(c, bot.Message, m.Channel, fmt.Sprintf("I already have %s.", i))
return true
}
var removed string
- if p.count() > p.config.Inventory.Max {
+ max := p.config.GetInt("inventory.max", 10)
+ if p.count() > 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)
+ log.Error().Err(err).Msg("Error inserting new inventory item")
}
if removed != "" {
- p.bot.SendAction(m.Channel, fmt.Sprintf("dropped %s and took %s from %s", removed, i, m.User.Name))
+ p.bot.Send(c, bot.Action, 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))
+ p.bot.Send(c, bot.Action, m.Channel, fmt.Sprintf("takes %s from %s", i, m.User.Name))
}
return true
}
func checkerr(e error) {
if e != nil {
- log.Println(e)
+ log.Error().Err(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
-}
-
-func (p *InventoryPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/leftpad/leftpad.go b/plugins/leftpad/leftpad.go
index 12d7395..0c97399 100644
--- a/plugins/leftpad/leftpad.go
+++ b/plugins/leftpad/leftpad.go
@@ -20,19 +20,20 @@ type LeftpadPlugin struct {
}
// New creates a new LeftpadPlugin with the Plugin interface
-func New(bot bot.Bot) *LeftpadPlugin {
- p := LeftpadPlugin{
- bot: bot,
- config: bot.Config(),
+func New(b bot.Bot) *LeftpadPlugin {
+ p := &LeftpadPlugin{
+ bot: b,
+ config: b.Config(),
}
- return &p
+ b.Register(p, bot.Message, p.message)
+ return p
}
type leftpadResp struct {
Str string
}
-func (p *LeftpadPlugin) Message(message msg.Message) bool {
+func (p *LeftpadPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@@ -42,39 +43,22 @@ func (p *LeftpadPlugin) Message(message msg.Message) bool {
padchar := parts[1]
length, err := strconv.Atoi(parts[2])
if err != nil {
- p.bot.SendMessage(message.Channel, "Invalid padding number")
+ p.bot.Send(c, bot.Message, message.Channel, "Invalid padding number")
return true
}
- if length > p.config.LeftPad.MaxLen && p.config.LeftPad.MaxLen > 0 {
- msg := fmt.Sprintf("%s would kill me if I did that.", p.config.LeftPad.Who)
- p.bot.SendMessage(message.Channel, msg)
+ maxLen, who := p.config.GetInt("LeftPad.MaxLen", 50), p.config.Get("LeftPad.Who", "Putin")
+ if length > maxLen && maxLen > 0 {
+ msg := fmt.Sprintf("%s would kill me if I did that.", who)
+ p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
text := strings.Join(parts[3:], " ")
res := leftpad.LeftPad(text, length, padchar)
- p.bot.SendMessage(message.Channel, res)
+ p.bot.Send(c, bot.Message, message.Channel, res)
return true
}
return false
}
-
-func (p *LeftpadPlugin) Event(e string, message msg.Message) bool {
- return false
-}
-
-func (p *LeftpadPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *LeftpadPlugin) Help(e string, m []string) {
-}
-
-func (p *LeftpadPlugin) RegisterWeb() *string {
- // nothing to register
- return nil
-}
-
-func (p *LeftpadPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/leftpad/leftpad_test.go b/plugins/leftpad/leftpad_test.go
index 805f8f1..fcfc10b 100644
--- a/plugins/leftpad/leftpad_test.go
+++ b/plugins/leftpad/leftpad_test.go
@@ -3,6 +3,7 @@
package leftpad
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -13,12 +14,12 @@ import (
"github.com/velour/catbase/plugins/counter"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -31,75 +32,57 @@ func makePlugin(t *testing.T) (*LeftpadPlugin, *bot.MockBot) {
counter.New(mb)
p := New(mb)
assert.NotNil(t, p)
+ p.config.Set("LeftPad.MaxLen", "0")
return p, mb
}
func TestLeftpad(t *testing.T) {
p, mb := makePlugin(t)
- p.Message(makeMessage("!leftpad test 8 test"))
+ p.message(makeMessage("!leftpad test 8 test"))
assert.Contains(t, mb.Messages[0], "testtest")
assert.Len(t, mb.Messages, 1)
}
func TestBadNumber(t *testing.T) {
p, mb := makePlugin(t)
- p.Message(makeMessage("!leftpad test fuck test"))
+ p.message(makeMessage("!leftpad test fuck test"))
assert.Contains(t, mb.Messages[0], "Invalid")
assert.Len(t, mb.Messages, 1)
}
func TestNotCommand(t *testing.T) {
p, mb := makePlugin(t)
- p.Message(makeMessage("leftpad test fuck test"))
+ p.message(makeMessage("leftpad test fuck test"))
assert.Len(t, mb.Messages, 0)
}
func TestNoMaxLen(t *testing.T) {
p, mb := makePlugin(t)
- p.Message(makeMessage("!leftpad dicks 100 dicks"))
+ p.config.Set("LeftPad.MaxLen", "0")
+ p.message(makeMessage("!leftpad dicks 100 dicks"))
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "dicks")
}
func Test50Padding(t *testing.T) {
p, mb := makePlugin(t)
- p.config.LeftPad.MaxLen = 50
- p.Message(makeMessage("!leftpad dicks 100 dicks"))
+ p.config.Set("LeftPad.MaxLen", "50")
+ assert.Equal(t, 50, p.config.GetInt("LeftPad.MaxLen", 100))
+ p.message(makeMessage("!leftpad dicks 100 dicks"))
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "kill me")
}
func TestUnder50Padding(t *testing.T) {
p, mb := makePlugin(t)
- p.config.LeftPad.MaxLen = 50
- p.Message(makeMessage("!leftpad dicks 49 dicks"))
+ p.config.Set("LeftPad.MaxLen", "50")
+ p.message(makeMessage("!leftpad dicks 49 dicks"))
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "dicks")
}
func TestNotPadding(t *testing.T) {
p, mb := makePlugin(t)
- p.Message(makeMessage("!lololol"))
+ p.message(makeMessage("!lololol"))
assert.Len(t, mb.Messages, 0)
}
-
-func TestHelp(t *testing.T) {
- p, mb := makePlugin(t)
- p.Help("channel", []string{})
- assert.Len(t, mb.Messages, 0)
-}
-
-func TestBotMessage(t *testing.T) {
- p, _ := makePlugin(t)
- assert.False(t, p.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- p, _ := makePlugin(t)
- assert.False(t, p.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- p, _ := makePlugin(t)
- assert.Nil(t, p.RegisterWeb())
-}
diff --git a/plugins/nerdepedia/nerdepedia.go b/plugins/nerdepedia/nerdepedia.go
index 8b21aec..d463425 100644
--- a/plugins/nerdepedia/nerdepedia.go
+++ b/plugins/nerdepedia/nerdepedia.go
@@ -27,29 +27,32 @@ type NerdepediaPlugin struct {
}
// NewNerdepediaPlugin creates a new NerdepediaPlugin with the Plugin interface
-func New(bot bot.Bot) *NerdepediaPlugin {
- return &NerdepediaPlugin{
- bot: bot,
- config: bot.Config(),
+func New(b bot.Bot) *NerdepediaPlugin {
+ np := &NerdepediaPlugin{
+ bot: b,
+ config: b.Config(),
}
+ b.Register(np, bot.Message, np.message)
+ b.Register(np, bot.Help, np.help)
+ return np
}
// 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 *NerdepediaPlugin) Message(message msg.Message) bool {
+func (p *NerdepediaPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
lowerCase := strings.ToLower(message.Body)
query := ""
if lowerCase == "may the force be with you" || lowerCase == "help me obi-wan" {
query = "http://starwars.wikia.com/wiki/Special:Random"
} else if lowerCase == "beam me up scotty" || lowerCase == "live long and prosper" {
query = "http://memory-alpha.wikia.com/wiki/Special:Random"
- } else if lowerCase == "bless the maker" || lowerCase == "i must not fear" {
+ } else if lowerCase == "bless the maker" || lowerCase == "i must not fear" || lowerCase == "the spice must flow" {
query = "http://dune.wikia.com/wiki/Special:Random"
} else if lowerCase == "my precious" || lowerCase == "one ring to rule them all" || lowerCase == "one does not simply walk into mordor" {
query = "http://lotr.wikia.com/wiki/Special:Random"
- } else if lowerCase == "gotta catch em all" {
- query = "https://bulbapedia.bulbagarden.net/wiki/Special:Random"
+ } else if lowerCase == "pikachu i choose you" || lowerCase == "gotta catch em all" {
+ query = "http://pokemon.wikia.com/wiki/Special:Random"
}
if query != "" {
@@ -78,7 +81,7 @@ func (p *NerdepediaPlugin) Message(message msg.Message) bool {
}
if description != "" && link != "" {
- p.bot.SendMessage(message.Channel, fmt.Sprintf("%s (%s)", description, link))
+ p.bot.Send(c, bot.Message, message.Channel, fmt.Sprintf("%s (%s)", description, link))
return true
}
}
@@ -87,23 +90,7 @@ func (p *NerdepediaPlugin) Message(message msg.Message) bool {
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *NerdepediaPlugin) Help(channel string, parts []string) {
- p.bot.SendMessage(channel, "nerd stuff")
+func (p *NerdepediaPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "nerd stuff")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *NerdepediaPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *NerdepediaPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *NerdepediaPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *NerdepediaPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/nerdepedia/nerdepeida_test.go b/plugins/nerdepedia/nerdepeida_test.go
index 04d3ad0..a135a31 100644
--- a/plugins/nerdepedia/nerdepeida_test.go
+++ b/plugins/nerdepedia/nerdepeida_test.go
@@ -3,6 +3,7 @@
package nerdepedia
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -25,11 +26,38 @@ func makeMessage(payload string) msg.Message {
}
}
-func TestObiWan(t *testing.T) {
+func TestWars(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("help me obi-wan"))
+ res := c.message(makeMessage("help me obi-wan"))
+ assert.Len(t, mb.Messages, 1)
+ assert.True(t, res)
+}
+
+func TestTrek(t *testing.T) {
+ mb := bot.NewMockBot()
+ c := New(mb)
+ assert.NotNil(t, c)
+ res := c.message(makeMessage("live long and prosper"))
+ assert.Len(t, mb.Messages, 1)
+ assert.True(t, res)
+}
+
+func TestDune(t *testing.T) {
+ mb := bot.NewMockBot()
+ c := New(mb)
+ assert.NotNil(t, c)
+ res := c.message(makeMessage("bless the maker"))
+ assert.Len(t, mb.Messages, 1)
+ assert.True(t, res)
+}
+
+func TestPoke(t *testing.T) {
+ mb := bot.NewMockBot()
+ c := New(mb)
+ assert.NotNil(t, c)
+ res := c.message(makeMessage("gotta catch em all"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}
diff --git a/plugins/newsbid/newsbid.go b/plugins/newsbid/newsbid.go
new file mode 100644
index 0000000..67128bb
--- /dev/null
+++ b/plugins/newsbid/newsbid.go
@@ -0,0 +1,115 @@
+package newsbid
+
+import (
+ "fmt"
+ "github.com/jmoiron/sqlx"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/plugins/newsbid/webshit"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+type NewsBid struct {
+ bot bot.Bot
+ db *sqlx.DB
+ ws *webshit.Webshit
+}
+
+func New(b bot.Bot) *NewsBid {
+ ws := webshit.New(b.DB())
+ p := &NewsBid{
+ bot: b,
+ db: b.DB(),
+ ws: ws,
+ }
+ p.bot.Register(p, bot.Message, p.message)
+ return p
+}
+
+func (p *NewsBid) message(conn bot.Connector, k bot.Kind, message msg.Message, args ...interface{}) bool {
+ body := strings.ToLower(message.Body)
+ ch := message.Channel
+ if message.Command && body == "balance" {
+ bal := p.ws.GetBalance(message.User.Name)
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("%s, your current balance is %d.",
+ message.User.Name, bal))
+ return true
+ }
+ if message.Command && body == "bids" {
+ bids, err := p.ws.GetAllBids()
+ if err != nil {
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error getting bids: %s", err))
+ return true
+ }
+ if len(bids) == 0 {
+ p.bot.Send(conn, bot.Message, ch, "No bids to report.")
+ return true
+ }
+ sort.Slice(bids, func(i, j int) bool { return bids[i].User < bids[j].User })
+ out := "Bids:\n"
+ for _, b := range bids {
+ out += fmt.Sprintf("%s bid %d on %s\n", b.User, b.Bid, b.Title)
+ }
+ p.bot.Send(conn, bot.Message, ch, out)
+ return true
+ }
+ if message.Command && body == "scores" {
+ bals, err := p.ws.GetAllBalances()
+ if err != nil {
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error getting bids: %s", err))
+ return true
+ }
+ if len(bals) == 0 {
+ p.bot.Send(conn, bot.Message, ch, "No balances to report.")
+ return true
+ }
+ out := "NGate balances:\n"
+ for _, b := range bals {
+ out += fmt.Sprintf("%s has a total score of %d with %d left to bid this session\n", b.User, b.Score, b.Balance)
+ }
+ p.bot.Send(conn, bot.Message, ch, out)
+ return true
+
+ }
+ if message.Command && strings.HasPrefix(body, "bid") {
+ parts := strings.Fields(body)
+ if len(parts) != 3 {
+ p.bot.Send(conn, bot.Message, ch, "You must bid with an amount and a URL.")
+ return true
+ }
+ amount, _ := strconv.Atoi(parts[1])
+ url := parts[2]
+ if bid, err := p.ws.Bid(message.User.Name, amount, url); err != nil {
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error placing bid: %s", err))
+ } else {
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Your bid has been placed on %s", bid.Title))
+ }
+ return true
+ }
+ if message.Command && body == "check ngate" {
+ p.check(conn, ch)
+ return true
+ }
+ return false
+}
+
+func (p *NewsBid) check(conn bot.Connector, ch string) {
+ wr, err := p.ws.Check()
+ if err != nil {
+ p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error checking ngate: %s", err))
+ return
+ }
+ for _, res := range wr {
+ msg := fmt.Sprintf("%s won %d for a score of %d",
+ res.User, res.Won, res.Score)
+ if len(res.WinningArticles) > 0 {
+ msg += "\nWinning articles: " + res.WinningArticles.Titles()
+ }
+ if len(res.LosingArticles) > 0 {
+ msg += "\nLosing articles: " + res.LosingArticles.Titles()
+ }
+ p.bot.Send(conn, bot.Message, ch, msg)
+ }
+}
diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go
new file mode 100644
index 0000000..40c28a2
--- /dev/null
+++ b/plugins/newsbid/webshit/webshit.go
@@ -0,0 +1,390 @@
+package webshit
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ hacknews "github.com/PaulRosset/go-hacknews"
+ "github.com/PuerkitoBio/goquery"
+ "github.com/jmoiron/sqlx"
+ "github.com/mmcdole/gofeed"
+ "github.com/rs/zerolog/log"
+)
+
+type Config struct {
+ HNFeed string
+ HNLimit int
+ BalanceReferesh int
+}
+
+var DefaultConfig = Config{
+ HNFeed: "topstories",
+ HNLimit: 10,
+ BalanceReferesh: 100,
+}
+
+type Webshit struct {
+ db *sqlx.DB
+ config Config
+}
+
+type Story struct {
+ Title string
+ URL string
+}
+
+type Stories []Story
+
+func (s Stories) Titles() string {
+ out := ""
+ for i, v := range s {
+ if i > 0 {
+ out += ", "
+ }
+ out += v.Title
+ }
+ return out
+}
+
+type Bid struct {
+ ID int
+ User string
+ Title string
+ URL string
+ Bid int
+ Placed int64
+}
+
+func (b Bid) PlacedParsed() time.Time {
+ return time.Unix(b.Placed, 0)
+}
+
+type Balance struct {
+ User string
+ Balance int
+ Score int
+}
+
+type WeeklyResult struct {
+ User string
+ Won int
+ WinningArticles Stories
+ LosingArticles Stories
+ Score int
+}
+
+func New(db *sqlx.DB) *Webshit {
+ return NewConfig(db, DefaultConfig)
+}
+
+func NewConfig(db *sqlx.DB, cfg Config) *Webshit {
+ w := &Webshit{db: db, config: cfg}
+ w.setup()
+ 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,
+ bid integer,
+ placed integer
+ )`)
+ w.db.MustExec(`create table if not exists webshit_balances (
+ user string primary key,
+ balance int,
+ score int
+ )`)
+}
+
+func (w *Webshit) Check() ([]WeeklyResult, error) {
+ stories, published, err := w.GetWeekly()
+ if err != nil {
+ return nil, err
+ }
+
+ var bids []Bid
+ if err = w.db.Select(&bids, `select user,title,url,bid from webshit_bids where placed < ?`,
+ published.Unix()); err != nil {
+ return nil, err
+ }
+
+ // Assuming no bids earlier than the weekly means there hasn't been a new weekly
+ if len(bids) == 0 {
+ return nil, fmt.Errorf("there are no bids against the current ngate post")
+ }
+
+ storyMap := map[string]Story{}
+ for _, s := range stories {
+ u, err := url.Parse(s.URL)
+ if err != nil {
+ log.Error().Err(err).Msg("couldn't parse URL")
+ continue
+ }
+ id := u.Query().Get("id")
+ storyMap[id] = s
+ }
+
+ wr := w.checkBids(bids, storyMap)
+
+ // Update all balance scores in a tx
+ if err := w.updateScores(wr); err != nil {
+ return nil, err
+ }
+
+ // Delete all those bids
+ if _, err = w.db.Exec(`delete from webshit_bids where placed < ?`,
+ published.Unix()); err != nil {
+ return nil, err
+ }
+
+ // Set all balances to 100
+ if _, err = w.db.Exec(`update webshit_balances set balance=?`,
+ w.config.BalanceReferesh); err != nil {
+ return nil, err
+ }
+
+ return wr, nil
+}
+
+func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) []WeeklyResult {
+
+ var wins []Bid
+ total, totalWinning := 0.0, 0.0
+ wr := map[string]WeeklyResult{}
+
+ for _, b := range bids {
+ score := w.GetScore(b.User)
+ if _, ok := wr[b.User]; !ok {
+ wr[b.User] = WeeklyResult{
+ User: b.User,
+ Score: score,
+ }
+ }
+ rec := wr[b.User]
+
+ u, err := url.Parse(b.URL)
+ if err != nil {
+ log.Error().Err(err).Msg("couldn't parse URL")
+ continue
+ }
+ id := u.Query().Get("id")
+
+ if s, ok := storyMap[id]; ok {
+ wins = append(wins, b)
+ rec.WinningArticles = append(rec.WinningArticles, s)
+ totalWinning += float64(b.Bid)
+ } else {
+ rec.LosingArticles = append(rec.LosingArticles, Story{b.Title, b.URL})
+ }
+ total += float64(b.Bid)
+ wr[b.User] = rec
+ }
+
+ for _, b := range wins {
+ payout := float64(b.Bid) / totalWinning * total
+ rec := wr[b.User]
+ rec.Won += int(payout)
+ rec.Score += int(payout)
+ wr[b.User] = rec
+ }
+
+ return wrMapToSlice(wr)
+}
+
+// GetHeadlines will return the current possible news headlines for bidding
+func (w *Webshit) GetHeadlines() ([]Story, error) {
+ news := hacknews.Initializer{Story: w.config.HNFeed, NbPosts: w.config.HNLimit}
+ ids, err := news.GetCodesStory()
+ if err != nil {
+ return nil, err
+ }
+ posts, err := news.GetPostStory(ids)
+ if err != nil {
+ return nil, err
+ }
+ var stories []Story
+ for _, p := range posts {
+ stories = append(stories, Story{
+ Title: p.Title,
+ URL: p.Url,
+ })
+ }
+ return stories, nil
+}
+
+// GetWeekly will return the headlines in the last webshit weekly report
+func (w *Webshit) GetWeekly() ([]Story, *time.Time, error) {
+ fp := gofeed.NewParser()
+ feed, err := fp.ParseURL("http://n-gate.com/hackernews/index.rss")
+ if err != nil {
+ return nil, nil, err
+ }
+ if len(feed.Items) <= 0 {
+ return nil, nil, fmt.Errorf("no webshit weekly found")
+ }
+
+ published := feed.Items[0].PublishedParsed
+
+ buf := bytes.NewBufferString(feed.Items[0].Description)
+ doc, err := goquery.NewDocumentFromReader(buf)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var items []Story
+ doc.Find(".storylink").Each(func(i int, s *goquery.Selection) {
+ story := Story{
+ Title: s.Find("a").Text(),
+ URL: s.SiblingsFiltered(".small").First().Find("a").AttrOr("href", ""),
+ }
+ items = append(items, story)
+ log.Debug().
+ Str("URL", story.URL).
+ Str("Title", story.Title).
+ Msg("Parsed webshit story")
+ })
+
+ return items, published, nil
+}
+
+// GetBalances returns the current balance for all known users
+// Any unknown user has a default balance on their first bid
+func (w *Webshit) GetBalance(user string) int {
+ q := `select balance from webshit_balances where user=?`
+ var balance int
+ err := w.db.Get(&balance, q, user)
+ if err != nil {
+ return 100
+ }
+ 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) {
+ var bids []Bid
+ err := w.db.Select(&bids, `select * from webshit_bids`)
+ if err != nil {
+ return nil, err
+ }
+ return bids, nil
+}
+
+func (w *Webshit) GetAllBalances() ([]Balance, error) {
+ var balances []Balance
+ err := w.db.Select(&balances, `select * from webshit_balances`)
+ if err != nil {
+ return nil, err
+ }
+ return balances, nil
+}
+
+// Bid allows a user to place a bid on a particular story
+func (w *Webshit) Bid(user string, amount int, URL string) (Bid, error) {
+ bal := w.GetBalance(user)
+ if amount < 0 {
+ return Bid{}, fmt.Errorf("cannot bid less than 0")
+ }
+ if bal < amount {
+ return Bid{}, fmt.Errorf("cannot bid more than balance, %d", bal)
+ }
+ story, err := w.getStoryByURL(URL)
+ if err != nil {
+ return Bid{}, err
+ }
+
+ ts := time.Now().Unix()
+
+ tx := w.db.MustBegin()
+ _, err = tx.Exec(`insert into webshit_bids (user,title,url,bid,placed) values (?,?,?,?,?)`,
+ user, story.Title, story.URL, amount, ts)
+ if err != nil {
+ tx.Rollback()
+ 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
+ }
+ tx.Commit()
+
+ return Bid{
+ User: user,
+ Title: story.Title,
+ URL: story.URL,
+ Placed: ts,
+ }, err
+}
+
+// getStoryByURL scrapes the URL for a title
+func (w *Webshit) getStoryByURL(URL string) (Story, error) {
+ u, err := url.Parse(URL)
+ if err != nil {
+ return Story{}, err
+ }
+ if u.Host != "news.ycombinator.com" {
+ return Story{}, fmt.Errorf("expected HN link")
+ }
+ res, err := http.Get(URL)
+ if err != nil {
+ return Story{}, err
+ }
+ defer res.Body.Close()
+ if res.StatusCode != 200 {
+ return Story{}, fmt.Errorf("bad response code: %d", res.StatusCode)
+ }
+
+ // Load the HTML document
+ doc, err := goquery.NewDocumentFromReader(res.Body)
+ if err != nil {
+ return Story{}, err
+ }
+
+ // Find the review items
+ title := doc.Find("title").Text()
+ title = strings.ReplaceAll(title, " | Hacker News", "")
+ return Story{
+ Title: title,
+ URL: URL,
+ }, nil
+}
+
+func (w *Webshit) updateScores(results []WeeklyResult) error {
+ tx := w.db.MustBegin()
+ for _, res := range results {
+ if _, err := tx.Exec(`update webshit_balances set score=? where user=?`,
+ res.Score, res.User); err != nil {
+ tx.Rollback()
+ return err
+ }
+ }
+ err := tx.Commit()
+ return err
+}
+
+func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult {
+ var out = []WeeklyResult{}
+ for _, r := range wr {
+ out = append(out, r)
+ }
+ return out
+}
diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go
new file mode 100644
index 0000000..e69551c
--- /dev/null
+++ b/plugins/newsbid/webshit/webshit_test.go
@@ -0,0 +1,99 @@
+package webshit
+
+import (
+ "github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "github.com/stretchr/testify/assert"
+ "os"
+ "testing"
+
+ _ "github.com/mattn/go-sqlite3"
+)
+
+func init() {
+ log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+}
+
+func makeWS(t *testing.T) *Webshit {
+ db := sqlx.MustOpen("sqlite3", "file::memory:?mode=memory&cache=shared")
+ w := New(db)
+ assert.Equal(t, w.db, db)
+ return w
+}
+
+func TestWebshit_GetWeekly(t *testing.T) {
+ w := makeWS(t)
+ weekly, pub, err := w.GetWeekly()
+ t.Logf("Pub: %v", pub)
+ assert.NotNil(t, pub)
+ assert.Nil(t, err)
+ assert.NotEmpty(t, weekly)
+}
+
+func TestWebshit_GetHeadlines(t *testing.T) {
+ w := makeWS(t)
+ headlines, err := w.GetHeadlines()
+ assert.Nil(t, err)
+ assert.NotEmpty(t, headlines)
+}
+
+func TestWebshit_getStoryByURL(t *testing.T) {
+ w := makeWS(t)
+ expected := "Developer Tropes: “Google Does It”"
+ s, err := w.getStoryByURL("https://news.ycombinator.com/item?id=20432887")
+ assert.Nil(t, err)
+ assert.Equal(t, s.Title, expected)
+}
+
+func TestWebshit_getStoryByURL_BadURL(t *testing.T) {
+ w := makeWS(t)
+ _, err := w.getStoryByURL("https://google.com")
+ assert.Error(t, err)
+}
+
+func TestWebshit_GetBalance(t *testing.T) {
+ w := makeWS(t)
+ expected := 100
+ actual := w.GetBalance("foo")
+ assert.Equal(t, expected, actual)
+}
+
+func TestWebshit_checkBids(t *testing.T) {
+ w := makeWS(t)
+ bids := []Bid{
+ Bid{User: "foo", Title: "bar", URL: "https://baz/?id=1", Bid: 10},
+ Bid{User: "foo", Title: "bar2", URL: "http://baz/?id=2", Bid: 10},
+ }
+ storyMap := map[string]Story{
+ "1": Story{Title: "bar", URL: "http://baz/?id=1"},
+ }
+ result := w.checkBids(bids, storyMap)
+ assert.Len(t, result, 1)
+ if len(result) > 0 {
+ assert.Len(t, result[0].WinningArticles, 1)
+ assert.Len(t, result[0].LosingArticles, 1)
+ }
+}
+
+func TestWebshit_33PcWinner(t *testing.T) {
+ w := makeWS(t)
+ bids := []Bid{
+ Bid{User: "foo", Title: "bar", URL: "https://baz/?id=1", Bid: 10},
+ Bid{User: "foo", Title: "bar2", URL: "http://baz/?id=2", Bid: 10},
+ Bid{User: "bar", Title: "bar", URL: "http://baz/?id=1", Bid: 5},
+ }
+ storyMap := map[string]Story{
+ "1": Story{Title: "bar", URL: "http://baz/?id=1"},
+ }
+ result := w.checkBids(bids, storyMap)
+ assert.Len(t, result, 2)
+ if len(result) > 0 {
+ assert.Len(t, result[0].WinningArticles, 1)
+ assert.Len(t, result[0].LosingArticles, 1)
+ assert.Len(t, result[1].WinningArticles, 1)
+ assert.Len(t, result[1].LosingArticles, 0)
+ assert.Equal(t, result[0].Won, 16)
+ assert.Equal(t, result[1].Won, 8)
+ }
+}
diff --git a/plugins/picker/picker.go b/plugins/picker/picker.go
index 53b5f4b..51d6b3f 100644
--- a/plugins/picker/picker.go
+++ b/plugins/picker/picker.go
@@ -16,34 +16,37 @@ import (
)
type PickerPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
}
// NewPickerPlugin creates a new PickerPlugin with the Plugin interface
-func New(bot bot.Bot) *PickerPlugin {
- return &PickerPlugin{
- Bot: bot,
+func New(b bot.Bot) *PickerPlugin {
+ pp := &PickerPlugin{
+ bot: b,
}
+ b.Register(pp, bot.Message, pp.message)
+ b.Register(pp, bot.Help, pp.help)
+ return pp
}
// 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 *PickerPlugin) Message(message msg.Message) bool {
+func (p *PickerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !strings.HasPrefix(message.Body, "pick") {
return false
}
n, items, err := p.parse(message.Body)
if err != nil {
- p.Bot.SendMessage(message.Channel, err.Error())
+ p.bot.Send(c, bot.Message, message.Channel, err.Error())
return true
}
if n == 1 {
item := items[rand.Intn(len(items))]
out := fmt.Sprintf("I've chosen %q for you.", strings.TrimSpace(item))
- p.Bot.SendMessage(message.Channel, out)
+ p.bot.Send(c, bot.Message, message.Channel, out)
return true
}
@@ -59,7 +62,7 @@ func (p *PickerPlugin) Message(message msg.Message) bool {
fmt.Fprintf(&b, ", %q", item)
}
b.WriteString(" }")
- p.Bot.SendMessage(message.Channel, b.String())
+ p.bot.Send(c, bot.Message, message.Channel, b.String())
return true
}
@@ -108,23 +111,7 @@ func (p *PickerPlugin) parse(body string) (int, []string, error) {
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *PickerPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Choose from a list of options. Try \"pick {a,b,c}\".")
+func (p *PickerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Choose from a list of options. Try \"pick {a,b,c}\".")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *PickerPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *PickerPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *PickerPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *PickerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/picker/picker_test.go b/plugins/picker/picker_test.go
index 0c2dd60..c1901c6 100644
--- a/plugins/picker/picker_test.go
+++ b/plugins/picker/picker_test.go
@@ -3,6 +3,7 @@
package picker
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -29,7 +30,7 @@ func TestPick2(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!pick 2 { a, b,c}"))
+ res := c.message(makeMessage("!pick 2 { a, b,c}"))
assert.Len(t, mb.Messages, 1)
if !res {
t.Fatalf("expected a successful choice, got %q", mb.Messages[0])
@@ -40,7 +41,7 @@ func TestPickDefault(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- _ = c.Message(makeMessage("!pick { a}"))
+ _ = c.message(makeMessage("!pick { a}"))
assert.Len(t, mb.Messages, 1)
assert.Equal(t, `I've chosen "a" for you.`, mb.Messages[0])
}
diff --git a/plugins/plugins.go b/plugins/plugins.go
index 364629f..b6a4fd5 100644
--- a/plugins/plugins.go
+++ b/plugins/plugins.go
@@ -1,15 +1,3 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package plugins
-
-import "github.com/velour/catbase/bot/msg"
-
-// Plugin interface defines the methods needed to accept a plugin
-type Plugin interface {
- Message(message msg.Message) bool
- Event(kind string, message msg.Message) bool
- BotMessage(message msg.Message) bool
- LoadData()
- Help()
- RegisterWeb()
-}
diff --git a/plugins/reaction/reaction.go b/plugins/reaction/reaction.go
index 266a4ab..a4a5f55 100644
--- a/plugins/reaction/reaction.go
+++ b/plugins/reaction/reaction.go
@@ -11,36 +11,38 @@ import (
)
type ReactionPlugin struct {
- Bot bot.Bot
- Config *config.Config
+ bot bot.Bot
+ config *config.Config
}
-func New(bot bot.Bot) *ReactionPlugin {
- return &ReactionPlugin{
- Bot: bot,
- Config: bot.Config(),
+func New(b bot.Bot) *ReactionPlugin {
+ rp := &ReactionPlugin{
+ bot: b,
+ config: b.Config(),
}
+ b.Register(rp, bot.Message, rp.message)
+ return rp
}
-func (p *ReactionPlugin) Message(message msg.Message) bool {
+func (p *ReactionPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
harrass := false
- for _, nick := range p.Config.Reaction.HarrassList {
+ for _, nick := range p.config.GetArray("Reaction.HarrassList", []string{}) {
if message.User.Name == nick {
harrass = true
break
}
}
- chance := p.Config.Reaction.GeneralChance
+ chance := p.config.GetFloat64("Reaction.GeneralChance", 0.01)
negativeWeight := 1
if harrass {
- chance = p.Config.Reaction.HarrassChance
- negativeWeight = p.Config.Reaction.NegativeHarrassmentMultiplier
+ chance = p.config.GetFloat64("Reaction.HarrassChance", 0.05)
+ negativeWeight = p.config.GetInt("Reaction.NegativeHarrassmentMultiplier", 2)
}
if rand.Float64() < chance {
- numPositiveReactions := len(p.Config.Reaction.PositiveReactions)
- numNegativeReactions := len(p.Config.Reaction.NegativeReactions)
+ numPositiveReactions := len(p.config.GetArray("Reaction.PositiveReactions", []string{}))
+ numNegativeReactions := len(p.config.GetArray("Reaction.NegativeReactions", []string{}))
maxIndex := numPositiveReactions + numNegativeReactions*negativeWeight
@@ -49,33 +51,15 @@ func (p *ReactionPlugin) Message(message msg.Message) bool {
reaction := ""
if index < numPositiveReactions {
- reaction = p.Config.Reaction.PositiveReactions[index]
+ reaction = p.config.GetArray("Reaction.PositiveReactions", []string{})[index]
} else {
index -= numPositiveReactions
index %= numNegativeReactions
- reaction = p.Config.Reaction.NegativeReactions[index]
+ reaction = p.config.GetArray("Reaction.NegativeReactions", []string{})[index]
}
- p.Bot.React(message.Channel, reaction, message)
+ p.bot.Send(c, bot.Reaction, message.Channel, reaction, message)
}
return false
}
-
-func (p *ReactionPlugin) Help(channel string, parts []string) {
-
-}
-
-func (p *ReactionPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *ReactionPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *ReactionPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *ReactionPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/remember/remember.go b/plugins/remember/remember.go
new file mode 100644
index 0000000..c56a105
--- /dev/null
+++ b/plugins/remember/remember.go
@@ -0,0 +1,152 @@
+package remember
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/plugins/fact"
+)
+
+type RememberPlugin struct {
+ bot bot.Bot
+ log map[string][]msg.Message
+ db *sqlx.DB
+}
+
+func New(b bot.Bot) *RememberPlugin {
+ p := &RememberPlugin{
+ bot: b,
+ log: make(map[string][]msg.Message),
+ db: b.DB(),
+ }
+
+ b.Register(p, bot.Message, p.message)
+ b.Register(p, bot.Help, p.help)
+
+ return p
+}
+
+func (p *RememberPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ if strings.ToLower(message.Body) == "quote" && message.Command {
+ q := p.randQuote()
+ p.bot.Send(c, bot.Message, message.Channel, q)
+
+ // is it evil not to remember that the user said quote?
+ return true
+ }
+
+ user := message.User
+ parts := strings.Fields(message.Body)
+
+ if message.Command && len(parts) >= 3 &&
+ strings.ToLower(parts[0]) == "remember" {
+ // we have a remember!
+ // look through the logs and find parts[1] as a user, if not,
+ // fuck this hoser
+ nick := parts[1]
+ snip := strings.Join(parts[2:], " ")
+ for i := len(p.log[message.Channel]) - 1; i >= 0; i-- {
+ entry := p.log[message.Channel][i]
+ log.Debug().Msgf("Comparing %s:%s with %s:%s",
+ entry.User.Name, entry.Body, nick, snip)
+ if strings.ToLower(entry.User.Name) == strings.ToLower(nick) &&
+ strings.Contains(
+ strings.ToLower(entry.Body),
+ strings.ToLower(snip),
+ ) {
+ log.Debug().Msg("Found!")
+
+ var msg string
+ if entry.Action {
+ msg = fmt.Sprintf("*%s* %s", entry.User.Name, entry.Body)
+ } else {
+ msg = fmt.Sprintf("<%s> %s", entry.User.Name, entry.Body)
+ }
+
+ trigger := fmt.Sprintf("%s quotes", entry.User.Name)
+
+ fact := fact.Factoid{
+ Fact: strings.ToLower(trigger),
+ Verb: "reply",
+ Tidbit: msg,
+ Owner: user.Name,
+ Created: time.Now(),
+ Accessed: time.Now(),
+ Count: 0,
+ }
+ if err := fact.Save(p.db); err != nil {
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, message.Channel, "Tell somebody I'm broke.")
+ }
+
+ log.Info().
+ Str("msg", msg).
+ Msg("Remembering factoid")
+
+ // sorry, not creative with names so we're reusing msg
+ msg = fmt.Sprintf("Okay, %s, remembering '%s'.",
+ message.User.Name, msg)
+ p.bot.Send(c, bot.Message, message.Channel, msg)
+ p.recordMsg(message)
+ return true
+
+ }
+ }
+ p.bot.Send(c, bot.Message, message.Channel, "Sorry, I don't know that phrase.")
+ p.recordMsg(message)
+ return true
+ }
+
+ p.recordMsg(message)
+ return false
+}
+
+func (p *RememberPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ msg := "remember will let you quote your idiot friends. Just type " +
+ "!remember to remember what they said. Snippet can " +
+ "be any part of their message. Later on, you can ask for a random " +
+ "!quote."
+
+ p.bot.Send(c, bot.Message, message.Channel, msg)
+ return true
+}
+
+// deliver a random quote out of the db.
+// 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
+func (p *RememberPlugin) randQuote() string {
+
+ var f fact.Factoid
+ 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 {
+ log.Error().Err(err).Msg("Error getting quotes")
+ return "I had a problem getting your quote."
+ }
+ f.Created = time.Unix(tmpCreated, 0)
+ f.Accessed = time.Unix(tmpAccessed, 0)
+
+ return f.Tidbit
+}
+
+func (p *RememberPlugin) recordMsg(message msg.Message) {
+ log.Debug().Msgf("Logging message: %s: %s", message.User.Name, message.Body)
+ p.log[message.Channel] = append(p.log[message.Channel], message)
+}
diff --git a/plugins/fact/remember_test.go b/plugins/remember/remember_test.go
similarity index 73%
rename from plugins/fact/remember_test.go
rename to plugins/remember/remember_test.go
index 3cbc835..91fa566 100644
--- a/plugins/fact/remember_test.go
+++ b/plugins/remember/remember_test.go
@@ -1,6 +1,7 @@
-package fact
+package remember
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -8,6 +9,7 @@ import (
"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 {
@@ -23,10 +25,10 @@ func makeMessage(nick, payload string) msg.Message {
}
}
-func makePlugin(t *testing.T) (*RememberPlugin, *Factoid, *bot.MockBot) {
+func makePlugin(t *testing.T) (*RememberPlugin, *fact.FactoidPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
- f := New(mb) // for DB table
- p := NewRemember(mb)
+ f := fact.New(mb) // for DB table
+ p := New(mb)
assert.NotNil(t, p)
return p, f, mb
}
@@ -42,11 +44,11 @@ func TestCornerCaseBug(t *testing.T) {
p, _, mb := makePlugin(t)
for _, m := range msgs {
- p.Message(m)
+ p.message(&cli.CliPlugin{}, bot.Message, m)
}
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "horse dick")
- q, err := getSingleFact(mb.DB(), "user1 quotes")
+ q, err := fact.GetSingleFact(mb.DB(), "user1 quotes")
assert.Nil(t, err)
assert.Contains(t, q.Tidbit, "horse dick")
}
diff --git a/plugins/reminder/reminder.go b/plugins/reminder/reminder.go
index e4a83d0..d4003e9 100644
--- a/plugins/reminder/reminder.go
+++ b/plugins/reminder/reminder.go
@@ -5,13 +5,18 @@ package reminder
import (
"errors"
"fmt"
- "log"
"strconv"
"strings"
"sync"
"time"
"github.com/jmoiron/sqlx"
+ "github.com/rs/zerolog/log"
+
+ "github.com/olebedev/when"
+ "github.com/olebedev/when/rules/common"
+ "github.com/olebedev/when/rules/en"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
@@ -22,11 +27,12 @@ const (
)
type ReminderPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
db *sqlx.DB
mutex *sync.Mutex
timer *time.Timer
config *config.Config
+ when *when.Parser
}
type Reminder struct {
@@ -38,10 +44,8 @@ type Reminder struct {
channel string
}
-func New(bot bot.Bot) *ReminderPlugin {
- log.SetFlags(log.LstdFlags | log.Lshortfile)
- if bot.DBVersion() == 1 {
- if _, err := bot.DB().Exec(`create table if not exists reminders (
+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,
@@ -49,33 +53,51 @@ func New(bot bot.Bot) *ReminderPlugin {
remindWhen string,
channel string
);`); err != nil {
- log.Fatal(err)
- }
+ log.Fatal().Err(err)
}
dur, _ := time.ParseDuration("1h")
timer := time.NewTimer(dur)
timer.Stop()
+ w := when.New(nil)
+ w.Add(en.All...)
+ w.Add(common.All...)
+
plugin := &ReminderPlugin{
- Bot: bot,
- db: bot.DB(),
+ bot: b,
+ db: b.DB(),
mutex: &sync.Mutex{},
timer: timer,
- config: bot.Config(),
+ config: b.Config(),
+ when: w,
}
plugin.queueUpNextReminder()
- go reminderer(plugin)
+ go reminderer(b.DefaultConnector(), plugin)
+
+ b.Register(plugin, bot.Message, plugin.message)
+ b.Register(plugin, bot.Help, plugin.help)
return plugin
}
-func (p *ReminderPlugin) Message(message msg.Message) bool {
+func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
channel := message.Channel
from := message.User.Name
+ var dur, dur2 time.Duration
+ t, err := p.when.Parse(message.Body, time.Now())
+ // Allowing err to fallthrough for other parsing
+ if t != nil && err == nil {
+ t2 := t.Time.Sub(time.Now()).String()
+ message.Body = string(message.Body[0:t.Index]) + t2 + string(message.Body[t.Index+len(t.Text):])
+ log.Debug().
+ Str("body", message.Body).
+ Str("text", t.Text).
+ Msg("Got time request")
+ }
parts := strings.Fields(message.Body)
if len(parts) >= 5 {
@@ -85,17 +107,16 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
who = from
}
- dur, err := time.ParseDuration(parts[3])
+ dur, err = time.ParseDuration(parts[3])
if err != nil {
- p.Bot.SendMessage(channel, "Easy cowboy, not sure I can parse that duration.")
+ p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
return true
}
operator := strings.ToLower(parts[2])
-
doConfirm := true
- if operator == "in" {
+ if operator == "in" || operator == "at" || operator == "on" {
//one off reminder
//remind who in dur blah
when := time.Now().UTC().Add(dur)
@@ -113,9 +134,10 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
} else if operator == "every" && strings.ToLower(parts[4]) == "for" {
//batch add, especially for reminding msherms to buy a kit
//remind who every dur for dur2 blah
- dur2, err := time.ParseDuration(parts[5])
+ dur2, err = time.ParseDuration(parts[5])
if err != nil {
- p.Bot.SendMessage(channel, "Easy cowboy, not sure I can parse that duration.")
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
return true
}
@@ -123,9 +145,10 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
endTime := time.Now().UTC().Add(dur2)
what := strings.Join(parts[6:], " ")
+ max := p.config.GetInt("Reminder.MaxBatchAdd", 10)
for i := 0; when.Before(endTime); i++ {
- if i >= p.config.Reminder.MaxBatchAdd {
- p.Bot.SendMessage(channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.")
+ if i >= max {
+ p.bot.Send(c, bot.Message, channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.")
doConfirm = false
break
}
@@ -142,14 +165,14 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
when = when.Add(dur)
}
} else {
- p.Bot.SendMessage(channel, "Easy cowboy, not sure I comprehend what you're asking.")
+ p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I comprehend what you're asking.")
return true
}
if doConfirm && from == who {
- p.Bot.SendMessage(channel, fmt.Sprintf("Okay. I'll remind you."))
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Okay. I'll remind you."))
} else if doConfirm {
- p.Bot.SendMessage(channel, fmt.Sprintf("Sure %s, I'll remind %s.", from, who))
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Sure %s, I'll remind %s.", from, who))
}
p.queueUpNextReminder()
@@ -169,22 +192,22 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
}
}
if err != nil {
- p.Bot.SendMessage(channel, "listing failed.")
+ p.bot.Send(c, bot.Message, channel, "listing failed.")
} else {
- p.Bot.SendMessage(channel, response)
+ p.bot.Send(c, bot.Message, channel, response)
}
return true
} else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" {
id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
- p.Bot.SendMessage(channel, fmt.Sprintf("couldn't parse id: %s", parts[2]))
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("couldn't parse id: %s", parts[2]))
} else {
err := p.deleteReminder(id)
if err == nil {
- p.Bot.SendMessage(channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2]))
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2]))
} else {
- p.Bot.SendMessage(channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2]))
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2]))
}
}
return true
@@ -193,20 +216,9 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
return false
}
-func (p *ReminderPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Pester someone with a reminder. Try \"remind in message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration")
-}
-
-func (p *ReminderPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *ReminderPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *ReminderPlugin) RegisterWeb() *string {
- return nil
+func (p *ReminderPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Pester someone with a reminder. Try \"remind in message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration")
+ return true
}
func (p *ReminderPlugin) getNextReminder() *Reminder {
@@ -214,7 +226,7 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
defer p.mutex.Unlock()
rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;")
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil
}
defer rows.Close()
@@ -223,19 +235,19 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
var reminder *Reminder
for rows.Next() {
if once {
- log.Print("somehow got multiple rows")
+ 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.Print(err)
+ log.Error().Err(err)
return nil
}
reminder.when, err = time.Parse(TIMESTAMP, when)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return nil
}
@@ -252,7 +264,7 @@ func (p *ReminderPlugin) addReminder(reminder *Reminder) error {
reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
}
return err
}
@@ -262,7 +274,7 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
defer p.mutex.Unlock()
res, err := p.db.Exec(`delete from reminders where id = ?;`, id)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
} else {
if affected, err := res.RowsAffected(); err != nil {
return err
@@ -273,12 +285,28 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
return err
}
-func (p *ReminderPlugin) getRemindersFormatted(queryString string) (string, error) {
+func (p *ReminderPlugin) getRemindersFormatted(filter string) (string, error) {
+ 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()
defer p.mutex.Unlock()
+
+ var total int
+ err := p.db.Get(&total, countString)
+ if err != nil {
+ log.Error().Err(err)
+ return "", nil
+ }
+
+ if total == 0 {
+ return "no pending reminders", nil
+ }
+
rows, err := p.db.Query(queryString)
if err != nil {
- log.Print(err)
+ log.Error().Err(err)
return "", nil
}
defer rows.Close()
@@ -294,23 +322,25 @@ func (p *ReminderPlugin) getRemindersFormatted(queryString string) (string, erro
reminders += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, when, reminder.id)
counter++
}
- if counter == 1 {
- return "no pending reminders", nil
+
+ remaining := total - max
+ if remaining > 0 {
+ reminders += fmt.Sprintf("...%d more...\n", remaining)
}
return reminders, nil
}
func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) {
- return p.getRemindersFormatted("select id, fromWho, toWho, what, remindWhen from reminders order by remindWhen asc;")
+ return p.getRemindersFormatted("")
}
func (p *ReminderPlugin) getAllRemindersFromMeFormatted(channel, me string) (string, error) {
- return p.getRemindersFormatted(fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders where fromWho = '%s' order by remindWhen asc;", me))
+ return p.getRemindersFormatted(fmt.Sprintf("where fromWho = '%s'", me))
}
func (p *ReminderPlugin) getAllRemindersToMeFormatted(channel, me string) (string, error) {
- return p.getRemindersFormatted(fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders where toWho = '%s' order by remindWhen asc;", me))
+ return p.getRemindersFormatted(fmt.Sprintf("where toWho = '%s'", me))
}
func (p *ReminderPlugin) queueUpNextReminder() {
@@ -321,7 +351,7 @@ func (p *ReminderPlugin) queueUpNextReminder() {
}
}
-func reminderer(p *ReminderPlugin) {
+func reminderer(c bot.Connector, p *ReminderPlugin) {
for {
<-p.timer.C
@@ -336,17 +366,16 @@ func reminderer(p *ReminderPlugin) {
message = fmt.Sprintf("Hey %s, %s wanted you to be reminded: %s", reminder.who, reminder.from, reminder.what)
}
- p.Bot.SendMessage(reminder.channel, message)
+ p.bot.Send(c, bot.Message, reminder.channel, message)
if err := p.deleteReminder(reminder.id); err != nil {
- log.Print(reminder.id)
- log.Print(err)
- log.Fatal("this will cause problems, we need to stop now.")
+ log.Error().
+ Int64("id", reminder.id).
+ Err(err).
+ Msg("this will cause problems, we need to stop now.")
}
}
p.queueUpNextReminder()
}
}
-
-func (p *ReminderPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/reminder/reminder_test.go b/plugins/reminder/reminder_test.go
index afafd5d..04a4622 100644
--- a/plugins/reminder/reminder_test.go
+++ b/plugins/reminder/reminder_test.go
@@ -4,6 +4,7 @@ package reminder
import (
"fmt"
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
"time"
@@ -14,25 +15,16 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
- isCmd := strings.HasPrefix(payload, "!")
- if isCmd {
- payload = payload[1:]
- }
- return msg.Message{
- User: &user.User{Name: "tester"},
- Channel: "test",
- Body: payload,
- Command: isCmd,
- }
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
+ return makeMessageBy(payload, "tester")
}
-func makeMessageBy(payload, by string) msg.Message {
+func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: by},
Channel: "test",
Body: payload,
@@ -40,11 +32,16 @@ func makeMessageBy(payload, by string) msg.Message {
}
}
-func TestMeReminder(t *testing.T) {
+func setup(t *testing.T) (*ReminderPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind me in 1s don't fail this test"))
+ r := New(mb)
+ mb.DB().MustExec(`delete from reminders; delete from config;`)
+ return r, mb
+}
+
+func TestMeReminder(t *testing.T) {
+ c, mb := setup(t)
+ res := c.message(makeMessage("!remind me in 1s don't fail this test"))
time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
@@ -53,10 +50,8 @@ func TestMeReminder(t *testing.T) {
}
func TestReminder(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser in 1s don't fail this test"))
+ c, mb := setup(t)
+ res := c.message(makeMessage("!remind testuser in 1s don't fail this test"))
time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
@@ -65,12 +60,10 @@ func TestReminder(t *testing.T) {
}
func TestReminderReorder(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser in 2s don't fail this test 2"))
+ c, mb := setup(t)
+ res := c.message(makeMessage("!remind testuser in 2s don't fail this test 2"))
assert.True(t, res)
- res = c.Message(makeMessage("!remind testuser in 1s don't fail this test 1"))
+ res = c.message(makeMessage("!remind testuser in 1s don't fail this test 1"))
assert.True(t, res)
time.Sleep(5 * time.Second)
assert.Len(t, mb.Messages, 4)
@@ -81,34 +74,28 @@ func TestReminderReorder(t *testing.T) {
}
func TestReminderParse(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser in unparseable don't fail this test"))
+ c, mb := setup(t)
+ res := c.message(makeMessage("!remind testuser in unparseable don't fail this test"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
- assert.Contains(t, mb.Messages[0], "Easy cowboy, not sure I can parse that duration.")
+ assert.Contains(t, mb.Messages[0], "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
}
func TestEmptyList(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!list reminders"))
+ c, mb := setup(t)
+ res := c.message(makeMessage("!list reminders"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "no pending reminders")
}
func TestList(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser in 5m don't fail this test 1"))
+ c, mb := setup(t)
+ res := c.message(makeMessage("!remind testuser in 5m don't fail this test 1"))
assert.True(t, res)
- res = c.Message(makeMessage("!remind testuser in 5m don't fail this test 2"))
+ res = c.message(makeMessage("!remind testuser in 5m don't fail this test 2"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders"))
+ res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "1) tester -> testuser :: don't fail this test 1 @ ")
@@ -116,14 +103,12 @@ func TestList(t *testing.T) {
}
func TestListBy(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
+ c, mb := setup(t)
+ res := c.message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
- res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
+ res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders from testuser"))
+ res = c.message(makeMessage("!list reminders from testuser"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "don't fail this test 1 @ ")
@@ -131,14 +116,12 @@ func TestListBy(t *testing.T) {
}
func TestListTo(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
+ c, mb := setup(t)
+ res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
- res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
+ res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders to testuser"))
+ res = c.message(makeMessage("!list reminders to testuser"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.NotContains(t, mb.Messages[2], "don't fail this test 1 @ ")
@@ -146,55 +129,36 @@ func TestListTo(t *testing.T) {
}
func TestToEmptyList(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
+ c, mb := setup(t)
+ res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
- res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
+ res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders to test"))
+ res = c.message(makeMessage("!list reminders to test"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders")
}
func TestFromEmptyList(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
+ c, mb := setup(t)
+ res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
- res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
+ res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders from test"))
+ res = c.message(makeMessage("!list reminders from test"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders")
}
-func TestBatch(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- c.config.Reminder.MaxBatchAdd = 50
- assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser every 1ms for 5ms yikes"))
- assert.True(t, res)
- time.Sleep(2 * time.Second)
- assert.Len(t, mb.Messages, 6)
- for i := 0; i < 5; i++ {
- assert.Contains(t, mb.Messages[i+1], "Hey testuser, tester wanted you to be reminded: yikes")
- }
-}
-
func TestBatchMax(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- c.config.Reminder.MaxBatchAdd = 10
+ c, mb := setup(t)
+ c.config.Set("Reminder.MaxBatchAdd", "10")
assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser every 1h for 24h yikes"))
+ res := c.message(makeMessage("!remind testuser every 1h for 24h yikes"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders"))
+ res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
time.Sleep(6 * time.Second)
assert.Len(t, mb.Messages, 2)
@@ -206,14 +170,13 @@ func TestBatchMax(t *testing.T) {
}
func TestCancel(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
+ c, mb := setup(t)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!remind testuser in 1m don't fail this test"))
+ res := c.message(makeMessage("!remind testuser in 1m don't fail this test"))
assert.True(t, res)
- res = c.Message(makeMessage("!cancel reminder 1"))
+ res = c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res)
- res = c.Message(makeMessage("!list reminders"))
+ res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.")
@@ -222,40 +185,45 @@ func TestCancel(t *testing.T) {
}
func TestCancelMiss(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
+ c, mb := setup(t)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!cancel reminder 1"))
+ res := c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res)
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 1")
}
-func TestHelp(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
+func TestLimitList(t *testing.T) {
+ c, mb := setup(t)
+ c.config.Set("Reminder.MaxBatchAdd", "10")
+ c.config.Set("Reminder.MaxList", "25")
assert.NotNil(t, c)
- c.Help("channel", []string{})
+
+ //Someone can redo this with a single batch add, but I can't locally due to an old version of sqllite (maybe).
+ res := c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
+ assert.True(t, res)
+ res = c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
+ assert.True(t, res)
+ res = c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
+ assert.True(t, res)
+ res = c.message(makeMessage("!list reminders"))
+ assert.True(t, res)
+ assert.Len(t, mb.Messages, 4)
+ assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.")
+ assert.Contains(t, mb.Messages[1], "Sure tester, I'll remind testuser.")
+ assert.Contains(t, mb.Messages[2], "Sure tester, I'll remind testuser.")
+
+ for i := 0; i < 25; i++ {
+ assert.Contains(t, mb.Messages[3], fmt.Sprintf("%d) tester -> testuser :: don't fail this test", i+1))
+ }
+ assert.Contains(t, mb.Messages[3], "more...")
+
+ assert.NotContains(t, mb.Messages[3], "26) tester -> testuser")
+}
+
+func TestHelp(t *testing.T) {
+ c, mb := setup(t)
+ assert.NotNil(t, c)
+ c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.Nil(t, c.RegisterWeb())
-}
diff --git a/plugins/rpgORdie/rpgORdie.go b/plugins/rpgORdie/rpgORdie.go
index 7320046..527d808 100644
--- a/plugins/rpgORdie/rpgORdie.go
+++ b/plugins/rpgORdie/rpgORdie.go
@@ -20,7 +20,7 @@ const (
)
type RPGPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
listenFor map[string]*board
}
@@ -98,45 +98,35 @@ func (b *board) checkAndMove(dx, dy int) int {
}
func New(b bot.Bot) *RPGPlugin {
- return &RPGPlugin{
- Bot: b,
+ rpg := &RPGPlugin{
+ bot: b,
listenFor: map[string]*board{},
}
+ b.Register(rpg, bot.Message, rpg.message)
+ b.Register(rpg, bot.Reply, rpg.replyMessage)
+ b.Register(rpg, bot.Help, rpg.help)
+ return rpg
}
-func (p *RPGPlugin) Message(message msg.Message) bool {
+func (p *RPGPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "start rpg" {
b := NewRandomBoard()
- ts := p.Bot.SendMessage(message.Channel, b.toMessageString())
+ ts, _ := p.bot.Send(c, bot.Message, message.Channel, b.toMessageString())
p.listenFor[ts] = b
- p.Bot.ReplyToMessageIdentifier(message.Channel, "Over here.", ts)
+ p.bot.Send(c, bot.Reply, message.Channel, "Over here.", ts)
return true
}
return false
}
-func (p *RPGPlugin) LoadData() {
-
+func (p *RPGPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Go find a walkthrough or something.")
+ return true
}
-func (p *RPGPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Go find a walkthrough or something.")
-}
-
-func (p *RPGPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *RPGPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *RPGPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool {
- if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) {
+func (p *RPGPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ identifier := args[0].(string)
+ if strings.ToLower(message.User.Name) != strings.ToLower(p.bot.Config().Get("Nick", "bot")) {
if b, ok := p.listenFor[identifier]; ok {
var res int
@@ -155,12 +145,12 @@ func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool {
switch res {
case OK:
- p.Bot.Edit(message.Channel, b.toMessageString(), identifier)
+ p.bot.Send(c, bot.Edit, message.Channel, b.toMessageString(), identifier)
case WIN:
- p.Bot.Edit(message.Channel, b.toMessageString(), identifier)
- p.Bot.ReplyToMessageIdentifier(message.Channel, "congratulations, you beat the easiest level imaginable.", identifier)
+ p.bot.Send(c, bot.Edit, message.Channel, b.toMessageString(), identifier)
+ p.bot.Send(c, bot.Reply, message.Channel, "congratulations, you beat the easiest level imaginable.", identifier)
case INVALID:
- p.Bot.ReplyToMessageIdentifier(message.Channel, fmt.Sprintf("you can't move %s", message.Body), identifier)
+ p.bot.Send(c, bot.Reply, message.Channel, fmt.Sprintf("you can't move %s", message.Body), identifier)
}
return true
}
diff --git a/plugins/rpgORdie/rpgORdie_test.go b/plugins/rpgORdie/rpgORdie_test.go
index ddcd924..86f7a77 100644
--- a/plugins/rpgORdie/rpgORdie_test.go
+++ b/plugins/rpgORdie/rpgORdie_test.go
@@ -1,3 +1 @@
package rpgORdie
-
-import ()
diff --git a/plugins/rss/rss.go b/plugins/rss/rss.go
index 37a5b1d..41cdcc1 100644
--- a/plugins/rss/rss.go
+++ b/plugins/rss/rss.go
@@ -12,7 +12,7 @@ import (
)
type RSSPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
cache map[string]*cacheItem
shelfLife time.Duration
maxLines int
@@ -49,28 +49,31 @@ func (c *cacheItem) getCurrentPage(maxLines int) string {
return page
}
-func New(bot bot.Bot) *RSSPlugin {
- return &RSSPlugin{
- Bot: bot,
+func New(b bot.Bot) *RSSPlugin {
+ rss := &RSSPlugin{
+ bot: b,
cache: map[string]*cacheItem{},
- shelfLife: time.Minute * 20,
- maxLines: 5,
+ shelfLife: time.Minute * time.Duration(b.Config().GetInt("rss.shelfLife", 20)),
+ maxLines: b.Config().GetInt("rss.maxLines", 5),
}
+ b.Register(rss, bot.Message, rss.message)
+ b.Register(rss, bot.Help, rss.help)
+ return rss
}
-func (p *RSSPlugin) Message(message msg.Message) bool {
+func (p *RSSPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
tokens := strings.Fields(message.Body)
numTokens := len(tokens)
if numTokens == 2 && strings.ToLower(tokens[0]) == "rss" {
if item, ok := p.cache[strings.ToLower(tokens[1])]; ok && time.Now().Before(item.expiration) {
- p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines))
+ p.bot.Send(c, bot.Message, message.Channel, item.getCurrentPage(p.maxLines))
return true
} else {
fp := gofeed.NewParser()
feed, err := fp.ParseURL(tokens[1])
if err != nil {
- p.Bot.SendMessage(message.Channel, fmt.Sprintf("RSS error: %s", err.Error()))
+ p.bot.Send(c, bot.Message, message.Channel, fmt.Sprintf("RSS error: %s", err.Error()))
return true
}
item := &cacheItem{
@@ -86,7 +89,7 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
p.cache[strings.ToLower(tokens[1])] = item
- p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines))
+ p.bot.Send(c, bot.Message, message.Channel, item.getCurrentPage(p.maxLines))
return true
}
}
@@ -94,28 +97,8 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
return false
}
-func (p *RSSPlugin) LoadData() {
- // This bot has no data to load
-}
-
// Help responds to help requests. Every plugin must implement a help function.
-func (p *RSSPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "try '!rss http://rss.cnn.com/rss/edition.rss'")
+func (p *RSSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "try '!rss http://rss.cnn.com/rss/edition.rss'")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *RSSPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *RSSPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *RSSPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *RSSPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/rss/rss_test.go b/plugins/rss/rss_test.go
index 7c20fd1..110fdf2 100644
--- a/plugins/rss/rss_test.go
+++ b/plugins/rss/rss_test.go
@@ -2,6 +2,7 @@ package rss
import (
"fmt"
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -11,12 +12,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -28,7 +29,7 @@ func TestRSS(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss"))
+ res := c.message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}
@@ -38,7 +39,7 @@ func TestRSSPaging(t *testing.T) {
c := New(mb)
assert.NotNil(t, c)
for i := 0; i < 20; i++ {
- res := c.Message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss"))
+ res := c.message(makeMessage("!rss http://rss.cnn.com/rss/edition.rss"))
assert.True(t, res)
}
diff --git a/plugins/sisyphus/sisyphus.go b/plugins/sisyphus/sisyphus.go
index 926dfc1..4bb607f 100644
--- a/plugins/sisyphus/sisyphus.go
+++ b/plugins/sisyphus/sisyphus.go
@@ -2,12 +2,13 @@ package sisyphus
import (
"fmt"
- "log"
"math/rand"
"strconv"
"strings"
"time"
+ "github.com/rs/zerolog/log"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@@ -18,7 +19,7 @@ const (
)
type SisyphusPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
listenFor map[string]*game
}
@@ -37,54 +38,54 @@ type game struct {
nextAns int
}
-func NewRandomGame(bot bot.Bot, channel, who string) *game {
+func NewRandomGame(c bot.Connector, b bot.Bot, channel, who string) *game {
size := rand.Intn(9) + 2
g := game{
channel: channel,
- bot: bot,
+ bot: b,
who: who,
start: time.Now(),
size: size,
current: size / 2,
}
- g.id = bot.SendMessage(channel, g.toMessageString())
+ g.id, _ = b.Send(c, bot.Message, channel, g.toMessageString())
- g.schedulePush()
- g.scheduleDecrement()
+ g.schedulePush(c)
+ g.scheduleDecrement(c)
return &g
}
-func (g *game) scheduleDecrement() {
+func (g *game) scheduleDecrement(c bot.Connector) {
if g.timers[0] != nil {
g.timers[0].Stop()
}
- minDec := g.bot.Config().Sisyphus.MinDecrement
- maxDec := g.bot.Config().Sisyphus.MinDecrement
- g.nextDec = time.Now().Add(time.Duration((minDec + rand.Intn(maxDec))) * time.Minute)
+ minDec := g.bot.Config().GetInt("Sisyphus.MinDecrement", 10)
+ maxDec := g.bot.Config().GetInt("Sisyphus.MaxDecrement", 30)
+ g.nextDec = time.Now().Add(time.Duration(minDec+rand.Intn(maxDec)) * time.Minute)
go func() {
t := time.NewTimer(g.nextDec.Sub(time.Now()))
g.timers[0] = t
select {
case <-t.C:
- g.handleDecrement()
+ g.handleDecrement(c)
}
}()
}
-func (g *game) schedulePush() {
+func (g *game) schedulePush(c bot.Connector) {
if g.timers[1] != nil {
g.timers[1].Stop()
}
- minPush := g.bot.Config().Sisyphus.MinPush
- maxPush := g.bot.Config().Sisyphus.MaxPush
+ minPush := g.bot.Config().GetInt("Sisyphus.MinPush", 1)
+ maxPush := g.bot.Config().GetInt("Sisyphus.MaxPush", 10)
g.nextPush = time.Now().Add(time.Duration(rand.Intn(maxPush)+minPush) * time.Minute)
go func() {
t := time.NewTimer(g.nextPush.Sub(time.Now()))
g.timers[1] = t
select {
case <-t.C:
- g.handleNotify()
+ g.handleNotify(c)
}
}()
}
@@ -96,21 +97,21 @@ func (g *game) endGame() {
g.ended = true
}
-func (g *game) handleDecrement() {
+func (g *game) handleDecrement(c bot.Connector) {
g.current++
- g.bot.Edit(g.channel, g.toMessageString(), g.id)
+ g.bot.Send(c, bot.Edit, g.channel, g.toMessageString(), g.id)
if g.current > g.size-2 {
- g.bot.ReplyToMessageIdentifier(g.channel, "you lose", g.id)
+ g.bot.Send(c, bot.Reply, g.channel, "you lose", g.id)
msg := fmt.Sprintf("%s just lost the game after %s", g.who, time.Now().Sub(g.start))
- g.bot.SendMessage(g.channel, msg)
+ g.bot.Send(c, bot.Message, g.channel, msg)
g.endGame()
} else {
- g.scheduleDecrement()
+ g.scheduleDecrement(c)
}
}
-func (g *game) handleNotify() {
- g.bot.ReplyToMessageIdentifier(g.channel, "You can push now.\n"+g.generateQuestion(), g.id)
+func (g *game) handleNotify(c bot.Connector) {
+ g.bot.Send(c, bot.Reply, g.channel, "You can push now.\n"+g.generateQuestion(), g.id)
}
func (g *game) generateQuestion() string {
@@ -162,43 +163,37 @@ func (g *game) toMessageString() string {
}
func New(b bot.Bot) *SisyphusPlugin {
- return &SisyphusPlugin{
- Bot: b,
+ sp := &SisyphusPlugin{
+ bot: b,
listenFor: map[string]*game{},
}
+ b.Register(sp, bot.Message, sp.message)
+ b.Register(sp, bot.Reply, sp.replyMessage)
+ b.Register(sp, bot.Help, sp.help)
+ return sp
}
-func (p *SisyphusPlugin) Message(message msg.Message) bool {
+func (p *SisyphusPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "start sisyphus" {
- b := NewRandomGame(p.Bot, message.Channel, message.User.Name)
+ b := NewRandomGame(c, p.bot, message.Channel, message.User.Name)
p.listenFor[b.id] = b
- p.Bot.ReplyToMessageIdentifier(message.Channel, "Over here.", b.id)
+ p.bot.Send(c, bot.Reply, message.Channel, "Over here.", b.id)
return true
}
return false
}
-func (p *SisyphusPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "https://en.wikipedia.org/wiki/Sisyphus")
+func (p *SisyphusPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "https://en.wikipedia.org/wiki/Sisyphus")
+ return true
}
-func (p *SisyphusPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-func (p *SisyphusPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *SisyphusPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *SisyphusPlugin) ReplyMessage(message msg.Message, identifier string) bool {
- if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) {
+func (p *SisyphusPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ identifier := args[0].(string)
+ if strings.ToLower(message.User.Name) != strings.ToLower(p.bot.Config().Get("Nick", "bot")) {
if g, ok := p.listenFor[identifier]; ok {
- log.Printf("got message on %s: %+v", identifier, message)
+ log.Debug().Msgf("got message on %s: %+v", identifier, message)
if g.ended {
return false
@@ -211,18 +206,18 @@ func (p *SisyphusPlugin) ReplyMessage(message msg.Message, identifier string) bo
if time.Now().After(g.nextPush) {
if g.checkAnswer(message.Body) {
- p.Bot.Edit(message.Channel, g.toMessageString(), identifier)
- g.schedulePush()
+ p.bot.Send(c, bot.Edit, message.Channel, g.toMessageString(), identifier)
+ g.schedulePush(c)
msg := fmt.Sprintf("Ok. You can push again in %s", g.nextPush.Sub(time.Now()))
- p.Bot.ReplyToMessageIdentifier(message.Channel, msg, identifier)
+ p.bot.Send(c, bot.Reply, message.Channel, msg, identifier)
} else {
- p.Bot.ReplyToMessageIdentifier(message.Channel, "you lose", identifier)
+ p.bot.Send(c, bot.Reply, message.Channel, "you lose", identifier)
msg := fmt.Sprintf("%s just lost the sisyphus game after %s", g.who, time.Now().Sub(g.start))
- p.Bot.SendMessage(message.Channel, msg)
+ p.bot.Send(c, bot.Message, message.Channel, msg)
g.endGame()
}
} else {
- p.Bot.ReplyToMessageIdentifier(message.Channel, "you cannot push yet", identifier)
+ p.bot.Send(c, bot.Reply, message.Channel, "you cannot push yet", identifier)
}
return true
}
diff --git a/plugins/stats/stats.go b/plugins/stats/stats.go
deleted file mode 100644
index 81450b2..0000000
--- a/plugins/stats/stats.go
+++ /dev/null
@@ -1,279 +0,0 @@
-// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors.
-
-// Stats contains the plugin that allows the bot record chat statistics
-package stats
-
-import (
- "encoding/json"
- "fmt"
- "log"
- "net/http"
- "os"
- "strconv"
- "strings"
- "time"
-
- "github.com/boltdb/bolt"
- "github.com/velour/catbase/bot"
- "github.com/velour/catbase/bot/msg"
- "github.com/velour/catbase/config"
-)
-
-const (
- DayFormat = "2006-01-02"
- HourFormat = "2006-01-02-15"
- HourBucket = "hour"
- UserBucket = "user"
- SightingBucket = "sighting"
-)
-
-type StatsPlugin struct {
- bot bot.Bot
- config *config.Config
-}
-
-// New creates a new StatsPlugin with the Plugin interface
-func New(bot bot.Bot) *StatsPlugin {
- p := StatsPlugin{
- bot: bot,
- config: bot.Config(),
- }
- return &p
-
-}
-
-type stat struct {
- // date formatted: DayFormat
- day string
- // category
- bucket string
- // specific unique individual
- key string
- val value
-}
-
-func mkDay() string {
- return time.Now().Format(DayFormat)
-}
-
-// The value type is here in the future growth case that we might want to put a
-// struct of more interesting information into the DB
-type value int
-
-func (v value) Bytes() ([]byte, error) {
- b, err := json.Marshal(v)
- return b, err
-}
-
-func valueFromBytes(b []byte) (value, error) {
- var v value
- err := json.Unmarshal(b, &v)
- return v, err
-}
-
-type stats []stat
-
-// mkStat converts raw data to a stat struct
-// Expected a string representation of the date formatted: DayFormat
-func mkStat(day string, bucket, key, val []byte) (stat, error) {
- v, err := valueFromBytes(val)
- if err != nil {
- log.Printf("mkStat: error getting value from bytes: %s", err)
- return stat{}, err
- }
- return stat{
- day: day,
- bucket: string(bucket),
- key: string(key),
- val: v,
- }, nil
-}
-
-// Another future-proofing function I shouldn't have written
-func (v value) add(other value) value {
- return v + other
-}
-
-func openDB(path string) (*bolt.DB, error) {
- db, err := bolt.Open(path, 0600, &bolt.Options{
- Timeout: 1 * time.Second,
- })
- if err != nil {
- log.Printf("Couldn't open BoltDB for stats (%s): %s", path, err)
- return nil, err
- }
- return db, err
-}
-
-// statFromDB takes a location specification and returns the data at that path
-// Expected a string representation of the date formatted: DayFormat
-func statFromDB(path, day, bucket, key string) (stat, error) {
- db, err := openDB(path)
- if err != nil {
- return stat{}, err
- }
- defer db.Close()
- buk := []byte(bucket)
- k := []byte(key)
-
- tx, err := db.Begin(true)
- if err != nil {
- log.Println("statFromDB: Error beginning the Tx")
- return stat{}, err
- }
- defer tx.Rollback()
-
- d, err := tx.CreateBucketIfNotExists([]byte(day))
- if err != nil {
- log.Println("statFromDB: Error creating the bucket")
- return stat{}, err
- }
- b, err := d.CreateBucketIfNotExists(buk)
- if err != nil {
- log.Println("statFromDB: Error creating the bucket")
- return stat{}, err
- }
-
- v := b.Get(k)
-
- if err := tx.Commit(); err != nil {
- log.Println("statFromDB: Error commiting the Tx")
- return stat{}, err
- }
-
- if v == nil {
- return stat{day, bucket, key, 0}, nil
- }
-
- return mkStat(day, buk, k, v)
-}
-
-// toDB takes a stat and records it, adding to the value in the DB if necessary
-func (s stats) toDB(path string) error {
- db, err := openDB(path)
- if err != nil {
- return err
- }
- defer db.Close()
-
- for _, stat := range s {
- err = db.Update(func(tx *bolt.Tx) error {
- d, err := tx.CreateBucketIfNotExists([]byte(stat.day))
- if err != nil {
- log.Println("toDB: Error creating bucket")
- return err
- }
- b, err := d.CreateBucketIfNotExists([]byte(stat.bucket))
- if err != nil {
- log.Println("toDB: Error creating bucket")
- return err
- }
-
- valueInDB := b.Get([]byte(stat.key))
- if valueInDB != nil {
- val, err := valueFromBytes(valueInDB)
- if err != nil {
- log.Println("toDB: Error getting value from bytes")
- return err
- }
- stat.val = stat.val.add(val)
- }
-
- v, err := stat.val.Bytes()
- if err != nil {
- return err
- }
- if stat.key == "" {
- log.Println("Keys should not be empty")
- return nil
- }
- log.Printf("Putting value in: '%s' %b, %+v", stat.key, []byte(stat.key), stat)
- err = b.Put([]byte(stat.key), v)
- return err
- })
- if err != nil {
- return err
- }
- }
- return nil
-}
-
-func (p *StatsPlugin) record(message msg.Message) {
- statGenerators := []func(msg.Message) stats{
- p.mkUserStat,
- p.mkHourStat,
- p.mkChannelStat,
- p.mkSightingStat,
- }
-
- allStats := stats{}
-
- for _, mk := range statGenerators {
- stats := mk(message)
- if stats != nil {
- allStats = append(allStats, stats...)
- }
- }
-
- allStats.toDB(p.bot.Config().Stats.DBPath)
-}
-
-func (p *StatsPlugin) Message(message msg.Message) bool {
- p.record(message)
- return false
-}
-
-func (p *StatsPlugin) Event(e string, message msg.Message) bool {
- p.record(message)
- return false
-}
-
-func (p *StatsPlugin) BotMessage(message msg.Message) bool {
- p.record(message)
- return false
-}
-
-func (p *StatsPlugin) Help(e string, m []string) {
-}
-
-func (p *StatsPlugin) serveQuery(w http.ResponseWriter, r *http.Request) {
- f, err := os.Open(p.bot.Config().Stats.DBPath)
- defer f.Close()
- if err != nil {
- log.Printf("Error opening DB for web service: %s", err)
- fmt.Fprintf(w, "Error opening DB")
- return
- }
- http.ServeContent(w, r, "stats.db", time.Now(), f)
-}
-
-func (p *StatsPlugin) RegisterWeb() *string {
- http.HandleFunc("/stats", p.serveQuery)
- tmp := "/stats"
- return &tmp
-}
-
-func (p *StatsPlugin) mkUserStat(message msg.Message) stats {
- return stats{stat{mkDay(), UserBucket, message.User.Name, 1}}
-}
-
-func (p *StatsPlugin) mkHourStat(message msg.Message) stats {
- hr := time.Now().Hour()
- return stats{stat{mkDay(), HourBucket, strconv.Itoa(hr), 1}}
-}
-
-func (p *StatsPlugin) mkSightingStat(message msg.Message) stats {
- stats := stats{}
- for _, name := range p.bot.Config().Stats.Sightings {
- if strings.Contains(message.Body, name+" sighting") {
- stats = append(stats, stat{mkDay(), SightingBucket, name, 1})
- }
- }
- return stats
-}
-
-func (p *StatsPlugin) mkChannelStat(message msg.Message) stats {
- return stats{stat{mkDay(), "channel", message.Channel, 1}}
-}
-
-func (p *StatsPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/stats/stats_test.go b/plugins/stats/stats_test.go
deleted file mode 100644
index d5597f4..0000000
--- a/plugins/stats/stats_test.go
+++ /dev/null
@@ -1,290 +0,0 @@
-package stats
-
-import (
- "encoding/json"
- "os"
- "strings"
- "testing"
-
- "github.com/stretchr/testify/assert"
- "github.com/velour/catbase/bot"
- "github.com/velour/catbase/bot/msg"
- "github.com/velour/catbase/bot/user"
-)
-
-var dbPath = "test.db"
-
-func TestJSON(t *testing.T) {
- expected := 5
- b, err := json.Marshal(expected)
- assert.Nil(t, err)
- t.Logf("%+v", expected)
- t.Log(string(b))
-}
-
-func TestValueConversion(t *testing.T) {
- expected := value(5)
-
- b, err := expected.Bytes()
- assert.Nil(t, err)
-
- t.Log(string(b))
-
- actual, err := valueFromBytes(b)
- assert.Nil(t, err)
-
- assert.Equal(t, actual, expected)
-}
-
-func rmDB(t *testing.T) {
- err := os.Remove(dbPath)
- if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
- t.Fatal(err)
- }
-}
-
-func TestWithDB(t *testing.T) {
- rmDB(t)
-
- t.Run("TestDBReadWrite", func(t *testing.T) {
- day := mkDay()
- bucket := "testBucket"
- key := "testKey"
-
- expected := stats{stat{
- day,
- bucket,
- key,
- 1,
- }}
-
- err := expected.toDB(dbPath)
- assert.Nil(t, err)
-
- actual, err := statFromDB(dbPath, day, bucket, key)
- assert.Nil(t, err)
-
- assert.Equal(t, actual, expected[0])
-
- })
-
- rmDB(t)
-
- t.Run("TestDBAddStatInLoop", func(t *testing.T) {
- day := mkDay()
- bucket := "testBucket"
- key := "testKey"
- expected := value(25)
-
- statPack := stats{stat{
- day,
- bucket,
- key,
- 5,
- }}
-
- for i := 0; i < 5; i++ {
- err := statPack.toDB(dbPath)
- assert.Nil(t, err)
- }
-
- actual, err := statFromDB(dbPath, day, bucket, key)
- assert.Nil(t, err)
-
- assert.Equal(t, actual.val, expected)
- })
-
- rmDB(t)
-
- t.Run("TestDBAddStats", func(t *testing.T) {
- day := mkDay()
- bucket := "testBucket"
- key := "testKey"
- expected := value(5)
-
- statPack := stats{}
- for i := 0; i < 5; i++ {
- statPack = append(statPack, stat{
- day,
- bucket,
- key,
- 1,
- })
- }
-
- err := statPack.toDB(dbPath)
- assert.Nil(t, err)
-
- actual, err := statFromDB(dbPath, day, bucket, key)
- assert.Nil(t, err)
-
- assert.Equal(t, actual.val, expected)
- })
-
- rmDB(t)
-}
-
-func makeMessage(payload string) msg.Message {
- isCmd := strings.HasPrefix(payload, "!")
- if isCmd {
- payload = payload[1:]
- }
- return msg.Message{
- User: &user.User{Name: "tester"},
- Channel: "test",
- Body: payload,
- Command: isCmd,
- }
-}
-
-func testUserCounter(t *testing.T, count int) {
- day := mkDay()
- expected := value(count)
- mb := bot.NewMockBot()
- mb.Cfg.Stats.DBPath = dbPath
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("test"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
-}
-
-func TestMessages(t *testing.T) {
- _, err := os.Stat(dbPath)
- assert.NotNil(t, err)
-
- t.Run("TestOneUserCounter", func(t *testing.T) {
- day := mkDay()
- count := 5
- expected := value(count)
- mb := bot.NewMockBot()
- mb.Cfg.Stats.DBPath = dbPath
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("test"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
- })
-
- rmDB(t)
-
- t.Run("TestTenUserCounter", func(t *testing.T) {
- day := mkDay()
- count := 5
- expected := value(count)
- mb := bot.NewMockBot()
- mb.Cfg.Stats.DBPath = dbPath
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("test"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "user", "tester")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
- })
-
- rmDB(t)
-
- t.Run("TestChannelCounter", func(t *testing.T) {
- day := mkDay()
- count := 5
- expected := value(count)
- mb := bot.NewMockBot()
- mb.Cfg.Stats.DBPath = dbPath
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("test"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "channel", "test")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
- })
-
- rmDB(t)
-
- t.Run("TestSightingCounter", func(t *testing.T) {
- day := mkDay()
- count := 5
- expected := value(count)
- mb := bot.NewMockBot()
-
- mb.Cfg.Stats.DBPath = dbPath
- mb.Cfg.Stats.Sightings = []string{"user", "nobody"}
-
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("user sighting"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "sighting", "user")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
- })
-
- rmDB(t)
-
- t.Run("TestSightingCounterNoResults", func(t *testing.T) {
- day := mkDay()
- count := 5
- expected := value(0)
- mb := bot.NewMockBot()
-
- mb.Cfg.Stats.DBPath = dbPath
- mb.Cfg.Stats.Sightings = []string{}
-
- s := New(mb)
- assert.NotNil(t, s)
-
- for i := 0; i < count; i++ {
- s.Message(makeMessage("user sighting"))
- }
-
- _, err := os.Stat(dbPath)
- assert.Nil(t, err)
-
- stat, err := statFromDB(mb.Config().Stats.DBPath, day, "sighting", "user")
- assert.Nil(t, err)
- actual := stat.val
- assert.Equal(t, actual, expected)
- })
-
- rmDB(t)
-}
diff --git a/plugins/stock/stock.go b/plugins/stock/stock.go
new file mode 100644
index 0000000..f2c8ba8
--- /dev/null
+++ b/plugins/stock/stock.go
@@ -0,0 +1,94 @@
+package stock
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+)
+
+type StockPlugin struct {
+ bot bot.Bot
+ apiKey string
+}
+
+func New(b bot.Bot) *StockPlugin {
+ s := &StockPlugin{
+ bot: b,
+ apiKey: b.Config().GetString("Stock.API_KEY", "0E1DP61SJ7GF81IE"),
+ }
+ b.Register(s, bot.Message, s.message)
+ b.Register(s, bot.Help, s.help)
+ return s
+}
+
+type GlobalQuote struct {
+ Info StockInfo `json:"GlobalQuote"`
+}
+
+type StockInfo struct {
+ Symbol string `json:"symbol"`
+ Open string `json:"open"`
+ High string `json:"high"`
+ Low string `json:"low"`
+ Price string `json:"price"`
+ Volume string `json:"volume"`
+ LatestTradingDay string `json:"latesttradingday"`
+ PreviousClose string `json:"previousclose"`
+ Change string `json:"change"`
+ ChangePercent string `json:"changepercent"`
+}
+
+func (p *StockPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ if !message.Command {
+ return false
+ }
+
+ tokens := strings.Fields(message.Body)
+ numTokens := len(tokens)
+
+ if numTokens == 2 && strings.ToLower(tokens[0]) == "stock-price" {
+ query := fmt.Sprintf("https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=%s&apikey=%s", tokens[1], p.apiKey)
+
+ resp, err := http.Get(query)
+ if err != nil {
+ log.Fatal().Err(err).Msg("Failed to get stock info")
+ }
+ body, err := ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ if err != nil {
+ log.Fatal().Err(err).Msg("Error stock info body")
+ }
+
+ response := "Failed to retrieve data for stock symbol: " + tokens[1]
+
+ regex := regexp.MustCompile("[0-9][0-9]\\. ")
+
+ cleaned := regex.ReplaceAllString(string(body), "")
+ cleaned = strings.ReplaceAll(cleaned, " ", "")
+
+ var info GlobalQuote
+ err = json.Unmarshal([]byte(cleaned), &info)
+
+ if err == nil && strings.EqualFold(tokens[1], info.Info.Symbol) {
+ response = fmt.Sprintf("%s : $%s (%s)", tokens[1], info.Info.Price, info.Info.ChangePercent)
+ }
+
+ p.bot.Send(c, bot.Message, message.Channel, response)
+ return true
+ }
+
+ return false
+}
+
+func (p *StockPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "try '!stock-price SYMBOL'")
+ return true
+}
diff --git a/plugins/stock/stock_test.go b/plugins/stock/stock_test.go
new file mode 100644
index 0000000..914f6d2
--- /dev/null
+++ b/plugins/stock/stock_test.go
@@ -0,0 +1,45 @@
+package stock
+
+import (
+ "github.com/velour/catbase/plugins/cli"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+)
+
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
+ isCmd := strings.HasPrefix(payload, "!")
+ if isCmd {
+ payload = payload[1:]
+ }
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
+ User: &user.User{Name: "tester"},
+ Channel: "test",
+ Body: payload,
+ Command: isCmd,
+ }
+}
+
+func TestValid(t *testing.T) {
+ mb := bot.NewMockBot()
+ c := New(mb)
+ assert.NotNil(t, c)
+ res := c.message(makeMessage("!stock-price TWTR"))
+ assert.Len(t, mb.Messages, 1)
+ assert.True(t, res)
+ assert.Contains(t, mb.Messages[0], "TWTR : $")
+}
+
+func TestInvalid(t *testing.T) {
+ mb := bot.NewMockBot()
+ c := New(mb)
+ assert.NotNil(t, c)
+ res := c.message(makeMessage("!stock-price NOTREAL"))
+ assert.Len(t, mb.Messages, 1)
+ assert.True(t, res)
+ assert.Contains(t, mb.Messages[0], "Failed to retrieve data for stock symbol: NOTREAL")
+}
diff --git a/plugins/talker/talker.go b/plugins/talker/talker.go
index 143e17b..7dc6fa2 100644
--- a/plugins/talker/talker.go
+++ b/plugins/talker/talker.go
@@ -4,14 +4,20 @@ package talker
import (
"fmt"
- "math/rand"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/exec"
"strings"
+ "github.com/rs/zerolog/log"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/config"
)
-var goatse []string = []string{
+var goatse = []string{
"```* g o a t s e x * g o a t s e x * g o a t s e x *",
"g g",
"o / \\ \\ / \\ o",
@@ -40,28 +46,48 @@ var goatse []string = []string{
}
type TalkerPlugin struct {
- Bot bot.Bot
- enforceNicks bool
- sayings []string
+ bot bot.Bot
+ config *config.Config
+ sayings []string
}
-func New(bot bot.Bot) *TalkerPlugin {
- return &TalkerPlugin{
- Bot: bot,
- enforceNicks: bot.Config().EnforceNicks,
- sayings: bot.Config().WelcomeMsgs,
+func New(b bot.Bot) *TalkerPlugin {
+ tp := &TalkerPlugin{
+ bot: b,
+ config: b.Config(),
}
+ b.Register(tp, bot.Message, tp.message)
+ b.Register(tp, bot.Help, tp.help)
+ tp.registerWeb(b.DefaultConnector())
+ return tp
}
-func (p *TalkerPlugin) Message(message msg.Message) bool {
+func (p *TalkerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
channel := message.Channel
body := message.Body
lowermessage := strings.ToLower(body)
+ if message.Command && strings.HasPrefix(lowermessage, "cowsay") {
+ msg, err := p.cowSay(strings.TrimPrefix(message.Body, "cowsay "))
+ if err != nil {
+ p.bot.Send(c, bot.Message, channel, "Error running cowsay: %s", err)
+ return true
+ }
+ p.bot.Send(c, bot.Message, channel, msg)
+ return true
+ }
+
+ if message.Command && strings.HasPrefix(lowermessage, "list cows") {
+ cows := p.allCows()
+ m := fmt.Sprintf("Cows: %s", strings.Join(cows, ", "))
+ p.bot.Send(c, bot.Message, channel, m)
+ return true
+ }
+
// TODO: This ought to be space split afterwards to remove any punctuation
if message.Command && strings.HasPrefix(lowermessage, "say") {
msg := strings.TrimSpace(body[3:])
- p.Bot.SendMessage(channel, msg)
+ p.bot.Send(c, bot.Message, channel, msg)
return true
}
@@ -77,45 +103,85 @@ func (p *TalkerPlugin) Message(message msg.Message) bool {
line = strings.Replace(line, "{nick}", nick, 1)
output += line + "\n"
}
- p.Bot.SendMessage(channel, output)
- return true
- }
-
- if p.enforceNicks && len(message.User.Name) != 9 {
- msg := fmt.Sprintf("Hey %s, we really like to have 9 character nicks because we're crazy OCD and stuff.",
- message.User.Name)
- p.Bot.SendMessage(message.Channel, msg)
+ p.bot.Send(c, bot.Message, channel, output)
return true
}
return false
}
-func (p *TalkerPlugin) Help(channel string, parts []string) {
- p.Bot.SendMessage(channel, "Hi, this is talker. I like to talk about FredFelps!")
+func (p *TalkerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Hi, this is talker. I like to talk about FredFelps!")
+ return true
}
-// Empty event handler because this plugin does not do anything on event recv
-func (p *TalkerPlugin) Event(kind string, message msg.Message) bool {
- if kind == "JOIN" && strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) {
- if len(p.sayings) == 0 {
- return false
+func (p *TalkerPlugin) cowSay(text string) (string, error) {
+ fields := strings.Split(text, " ")
+ cow := "default"
+ if len(fields) > 1 && p.hasCow(fields[0]) {
+ cow = fields[0]
+ text = strings.Join(fields[1:], " ")
+ }
+ cmd := exec.Command("cowsay", "-f", cow, text)
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return "", err
+ }
+
+ if err = cmd.Start(); err != nil {
+ return "", err
+ }
+
+ output, err := ioutil.ReadAll(stdout)
+ if err != nil {
+ return "", err
+ }
+
+ return fmt.Sprintf("```%s```", output), nil
+}
+
+func (p *TalkerPlugin) hasCow(cow string) bool {
+ cows := p.allCows()
+ for _, c := range cows {
+ if strings.ToLower(cow) == c {
+ return true
}
- msg := fmt.Sprintf(p.sayings[rand.Intn(len(p.sayings))], message.User.Name)
- p.Bot.SendMessage(message.Channel, msg)
- return true
}
return false
}
-// Handler for bot's own messages
-func (p *TalkerPlugin) BotMessage(message msg.Message) bool {
- return false
+func (p *TalkerPlugin) allCows() []string {
+ f, err := os.Open(p.config.Get("talker.cowpath", "/usr/local/share/cows"))
+ if err != nil {
+ return []string{"default"}
+ }
+
+ files, err := f.Readdir(0)
+ if err != nil {
+ return []string{"default"}
+ }
+
+ cows := []string{}
+ for _, f := range files {
+ if strings.HasSuffix(f.Name(), ".cow") {
+ cows = append(cows, strings.TrimSuffix(f.Name(), ".cow"))
+ }
+ }
+ return cows
}
-// Register any web URLs desired
-func (p *TalkerPlugin) RegisterWeb() *string {
- return nil
+func (p *TalkerPlugin) registerWeb(c bot.Connector) {
+ http.HandleFunc("/slash/cowsay", func(w http.ResponseWriter, r *http.Request) {
+ r.ParseForm()
+ log.Debug().Msgf("Cowsay:\n%+v", r.PostForm.Get("text"))
+ channel := r.PostForm.Get("channel_id")
+ log.Debug().Msgf("channel: %s", channel)
+ msg, err := p.cowSay(r.PostForm.Get("text"))
+ if err != nil {
+ p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Error running cowsay: %s", err))
+ return
+ }
+ p.bot.Send(c, bot.Message, channel, msg)
+ w.WriteHeader(200)
+ })
}
-
-func (p *TalkerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/talker/talker_test.go b/plugins/talker/talker_test.go
index fd81aa6..cf3defe 100644
--- a/plugins/talker/talker_test.go
+++ b/plugins/talker/talker_test.go
@@ -3,6 +3,7 @@
package talker
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -29,7 +30,7 @@ func TestGoatse(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("goatse"))
+ res := c.message(makeMessage("goatse"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
@@ -38,7 +39,7 @@ func TestGoatseCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!goatse"))
+ res := c.message(makeMessage("!goatse"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e")
@@ -48,7 +49,7 @@ func TestGoatseWithNickCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!goatse seabass"))
+ res := c.message(makeMessage("!goatse seabass"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e")
@@ -59,7 +60,7 @@ func TestSay(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("say hello"))
+ res := c.message(makeMessage("say hello"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
@@ -68,78 +69,16 @@ func TestSayCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- res := c.Message(makeMessage("!say hello"))
+ res := c.message(makeMessage("!say hello"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "hello")
}
-func TestNineChars(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- c.enforceNicks = true
- assert.NotNil(t, c)
- res := c.Message(makeMessage("hello there"))
- assert.Len(t, mb.Messages, 1)
- assert.True(t, res)
- assert.Contains(t, mb.Messages[0], "OCD")
-}
-
-func TestWelcome(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- c.sayings = []string{"Hi"}
- assert.NotNil(t, c)
- res := c.Event("JOIN", makeMessage("hello there"))
- assert.Len(t, mb.Messages, 1)
- assert.True(t, res)
- assert.Contains(t, mb.Messages[0], "Hi")
-}
-
-func TestNoSayings(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- c.sayings = []string{}
- assert.NotNil(t, c)
- res := c.Event("JOIN", makeMessage("hello there"))
- assert.Len(t, mb.Messages, 0)
- assert.False(t, res)
-}
-
-func TestNonJoinEvent(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- res := c.Event("SPLURT", makeMessage("hello there"))
- assert.Len(t, mb.Messages, 0)
- assert.False(t, res)
-}
-
func TestHelp(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
- c.Help("channel", []string{})
+ c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
-
-func TestBotMessage(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.BotMessage(makeMessage("test")))
-}
-
-func TestEvent(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.False(t, c.Event("dummy", makeMessage("test")))
-}
-
-func TestRegisterWeb(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- assert.Nil(t, c.RegisterWeb())
-}
diff --git a/plugins/tell/tell.go b/plugins/tell/tell.go
index 05bab8a..a106574 100644
--- a/plugins/tell/tell.go
+++ b/plugins/tell/tell.go
@@ -16,32 +16,28 @@ type TellPlugin struct {
}
func New(b bot.Bot) *TellPlugin {
- return &TellPlugin{b, make(map[string][]string)}
+ tp := &TellPlugin{b, make(map[string][]string)}
+ b.Register(tp, bot.Message, tp.message)
+ return tp
}
-func (t *TellPlugin) Message(message msg.Message) bool {
+func (t *TellPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.HasPrefix(strings.ToLower(message.Body), "tell") {
parts := strings.Split(message.Body, " ")
target := strings.ToLower(parts[1])
newMessage := strings.Join(parts[2:], " ")
newMessage = fmt.Sprintf("Hey, %s. %s said: %s", target, message.User.Name, newMessage)
t.users[target] = append(t.users[target], newMessage)
- t.b.SendMessage(message.Channel, fmt.Sprintf("Okay. I'll tell %s.", target))
+ t.b.Send(c, bot.Message, message.Channel, fmt.Sprintf("Okay. I'll tell %s.", target))
return true
}
uname := strings.ToLower(message.User.Name)
if msg, ok := t.users[uname]; ok && len(msg) > 0 {
for _, m := range msg {
- t.b.SendMessage(message.Channel, string(m))
+ t.b.Send(c, bot.Message, message.Channel, string(m))
}
t.users[uname] = []string{}
return true
}
return false
}
-
-func (t *TellPlugin) Event(kind string, message msg.Message) bool { return false }
-func (t *TellPlugin) ReplyMessage(msg.Message, string) bool { return false }
-func (t *TellPlugin) BotMessage(message msg.Message) bool { return false }
-func (t *TellPlugin) Help(channel string, parts []string) {}
-func (t *TellPlugin) RegisterWeb() *string { return nil }
diff --git a/plugins/tldr/badwords.go b/plugins/tldr/badwords.go
new file mode 100644
index 0000000..2e7ef33
--- /dev/null
+++ b/plugins/tldr/badwords.go
@@ -0,0 +1,185 @@
+package tldr
+
+//nltk stopwords list, from some point...
+
+var THESE_ARE_NOT_THE_WORDS_YOU_ARE_LOOKING_FOR = []string{
+ "i",
+ "me",
+ "my",
+ "myself",
+ "we",
+ "our",
+ "ours",
+ "ourselves",
+ "you",
+ "you're",
+ "you've",
+ "you'll",
+ "you'd",
+ "your",
+ "yours",
+ "yourself",
+ "yourselves",
+ "he",
+ "him",
+ "his",
+ "himself",
+ "she",
+ "she's",
+ "her",
+ "hers",
+ "herself",
+ "it",
+ "it's",
+ "its",
+ "itself",
+ "they",
+ "them",
+ "their",
+ "theirs",
+ "themselves",
+ "what",
+ "which",
+ "who",
+ "whom",
+ "this",
+ "that",
+ "that'll",
+ "these",
+ "those",
+ "am",
+ "is",
+ "are",
+ "was",
+ "were",
+ "be",
+ "been",
+ "being",
+ "have",
+ "has",
+ "had",
+ "having",
+ "do",
+ "does",
+ "did",
+ "doing",
+ "a",
+ "an",
+ "the",
+ "and",
+ "but",
+ "if",
+ "or",
+ "because",
+ "as",
+ "until",
+ "while",
+ "of",
+ "at",
+ "by",
+ "for",
+ "with",
+ "about",
+ "against",
+ "between",
+ "into",
+ "through",
+ "during",
+ "before",
+ "after",
+ "above",
+ "below",
+ "to",
+ "from",
+ "up",
+ "down",
+ "in",
+ "out",
+ "on",
+ "off",
+ "over",
+ "under",
+ "again",
+ "further",
+ "then",
+ "once",
+ "here",
+ "there",
+ "when",
+ "where",
+ "why",
+ "how",
+ "all",
+ "any",
+ "both",
+ "each",
+ "few",
+ "more",
+ "most",
+ "other",
+ "some",
+ "such",
+ "no",
+ "nor",
+ "not",
+ "only",
+ "own",
+ "same",
+ "so",
+ "than",
+ "too",
+ "very",
+ "s",
+ "t",
+ "can",
+ "will",
+ "just",
+ "don",
+ "don't",
+ "should",
+ "should've",
+ "now",
+ "d",
+ "ll",
+ "m",
+ "o",
+ "re",
+ "ve",
+ "y",
+ "ain",
+ "aren",
+ "aren't",
+ "couldn",
+ "couldn't",
+ "didn",
+ "didn't",
+ "doesn",
+ "doesn't",
+ "hadn",
+ "hadn't",
+ "hasn",
+ "hasn't",
+ "haven",
+ "haven't",
+ "isn",
+ "isn't",
+ "ma",
+ "mightn",
+ "mightn't",
+ "mustn",
+ "mustn't",
+ "needn",
+ "needn't",
+ "shan",
+ "shan't",
+ "shouldn",
+ "shouldn't",
+ "wasn",
+ "wasn't",
+ "weren",
+ "weren't",
+ "won",
+ "won't",
+ "wouldn",
+ "wouldn't",
+}
diff --git a/plugins/tldr/tldr.go b/plugins/tldr/tldr.go
new file mode 100644
index 0000000..0bc3caf
--- /dev/null
+++ b/plugins/tldr/tldr.go
@@ -0,0 +1,180 @@
+package tldr
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+
+ "github.com/rs/zerolog/log"
+
+ "github.com/james-bowman/nlp"
+)
+
+type TLDRPlugin struct {
+ bot bot.Bot
+ history []history
+ index int
+ lastRequest time.Time
+}
+
+type history struct {
+ timestamp time.Time
+ user string
+ body string
+}
+
+func New(b bot.Bot) *TLDRPlugin {
+ plugin := &TLDRPlugin{
+ bot: b,
+ history: []history{},
+ index: 0,
+ lastRequest: time.Now().Add(-24 * time.Hour),
+ }
+ b.Register(plugin, bot.Message, plugin.message)
+ b.Register(plugin, bot.Help, plugin.help)
+ return plugin
+}
+
+func (p *TLDRPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ timeLimit := time.Duration(p.bot.Config().GetInt("TLDR.HourLimit", 1))
+ lowercaseMessage := strings.ToLower(message.Body)
+ if lowercaseMessage == "tl;dr" && p.lastRequest.After(time.Now().Add(-timeLimit*time.Hour)) {
+ p.bot.Send(c, bot.Message, message.Channel, "Slow down, cowboy. Read that tiny backlog.")
+ return true
+ } else if lowercaseMessage == "tl;dr" {
+ p.lastRequest = time.Now()
+ nTopics := p.bot.Config().GetInt("TLDR.Topics", 5)
+
+ stopWordSlice := p.bot.Config().GetArray("TLDR.StopWords", []string{})
+ if len(stopWordSlice) == 0 {
+ stopWordSlice = THESE_ARE_NOT_THE_WORDS_YOU_ARE_LOOKING_FOR
+ p.bot.Config().SetArray("TLDR.StopWords", stopWordSlice)
+ }
+
+ vectoriser := nlp.NewCountVectoriser(stopWordSlice...)
+ lda := nlp.NewLatentDirichletAllocation(nTopics)
+ pipeline := nlp.NewPipeline(vectoriser, lda)
+ docsOverTopics, err := pipeline.FitTransform(p.getTopics()...)
+
+ if err != nil {
+ log.Error().Err(err)
+ return false
+ }
+
+ bestScores := make([][]float64, nTopics)
+ bestDocs := make([][]history, nTopics)
+
+ supportingDocs := p.bot.Config().GetInt("TLDR.Support", 3)
+ for i := 0; i < nTopics; i++ {
+ bestScores[i] = make([]float64, supportingDocs)
+ bestDocs[i] = make([]history, supportingDocs)
+ }
+
+ dr, dc := docsOverTopics.Dims()
+ for topic := 0; topic < dr; topic++ {
+ minScore, minIndex := min(bestScores[topic])
+
+ for doc := 0; doc < dc; doc++ {
+ score := docsOverTopics.At(topic, doc)
+ if score > minScore {
+ bestScores[topic][minIndex] = score
+ bestDocs[topic][minIndex] = p.history[doc]
+ minScore, minIndex = min(bestScores[topic])
+ }
+ }
+ }
+
+ topicsOverWords := lda.Components()
+ tr, tc := topicsOverWords.Dims()
+
+ vocab := make([]string, len(vectoriser.Vocabulary))
+ for k, v := range vectoriser.Vocabulary {
+ vocab[v] = k
+ }
+
+ response := "Here you go captain 'too good to read backlog':\n"
+
+ for topic := 0; topic < tr; topic++ {
+ bestScore := -1.
+ bestTopic := ""
+ for word := 0; word < tc; word++ {
+ score := topicsOverWords.At(topic, word)
+ if score > bestScore {
+ bestScore = score
+ bestTopic = vocab[word]
+ }
+ }
+ response += fmt.Sprintf("\n*Topic #%d: %s*\n", topic, bestTopic)
+ for i := range bestDocs[topic] {
+ response += fmt.Sprintf("<%s>%s\n", bestDocs[topic][i].user, bestDocs[topic][i].body)
+ }
+
+ }
+
+ p.bot.Send(c, bot.Message, message.Channel, response)
+
+ return true
+ }
+
+ hist := history{
+ body: lowercaseMessage,
+ user: message.User.Name,
+ timestamp: time.Now(),
+ }
+ p.addHistory(hist)
+
+ return false
+}
+
+func (p *TLDRPlugin) addHistory(hist history) {
+ p.history = append(p.history, hist)
+ sz := len(p.history)
+ max := p.bot.Config().GetInt("TLDR.HistorySize", 1000)
+ keepHrs := time.Duration(p.bot.Config().GetInt("TLDR.KeepHours", 24))
+ // Clamp the size of the history
+ if sz > max {
+ p.history = p.history[len(p.history)-max:]
+ }
+ // Remove old entries
+ yesterday := time.Now().Add(-keepHrs * time.Hour)
+ begin := 0
+ for i, m := range p.history {
+ if !m.timestamp.Before(yesterday) {
+ begin = i - 1 // should keep this message
+ if begin < 0 {
+ begin = 0
+ }
+ break
+ }
+ }
+ p.history = p.history[begin:]
+}
+
+func (p *TLDRPlugin) getTopics() []string {
+ hist := []string{}
+ for _, h := range p.history {
+ hist = append(hist, h.body)
+ }
+ return hist
+}
+
+// Help responds to help requests. Every plugin must implement a help function.
+func (p *TLDRPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "tl;dr")
+ return true
+}
+
+func min(slice []float64) (float64, int) {
+ minVal := 1.
+ minIndex := -1
+ for index, val := range slice {
+ if val < minVal {
+ minVal = val
+ minIndex = index
+ }
+ }
+ return minVal, minIndex
+}
diff --git a/plugins/tldr/tldr_test.go b/plugins/tldr/tldr_test.go
new file mode 100644
index 0000000..4328463
--- /dev/null
+++ b/plugins/tldr/tldr_test.go
@@ -0,0 +1,101 @@
+package tldr
+
+import (
+ "github.com/velour/catbase/plugins/cli"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
+ "github.com/stretchr/testify/assert"
+ "github.com/velour/catbase/bot"
+ "github.com/velour/catbase/bot/msg"
+ "github.com/velour/catbase/bot/user"
+)
+
+func init() {
+ log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+}
+
+func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
+ isCmd := strings.HasPrefix(payload, "!")
+ if isCmd {
+ payload = payload[1:]
+ }
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
+ User: &user.User{Name: by},
+ Channel: "test",
+ Body: payload,
+ Command: isCmd,
+ }
+}
+
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
+ return makeMessageBy(payload, "tester")
+}
+
+func setup(t *testing.T) (*TLDRPlugin, *bot.MockBot) {
+ mb := bot.NewMockBot()
+ r := New(mb)
+ return r, mb
+}
+
+func Test(t *testing.T) {
+ c, mb := setup(t)
+ res := c.message(makeMessage("The quick brown fox jumped over the lazy dog"))
+ res = c.message(makeMessage("The cow jumped over the moon"))
+ res = c.message(makeMessage("The little dog laughed to see such fun"))
+ res = c.message(makeMessage("tl;dr"))
+ assert.True(t, res)
+ assert.Len(t, mb.Messages, 1)
+}
+
+func TestDoubleUp(t *testing.T) {
+ c, mb := setup(t)
+ res := c.message(makeMessage("The quick brown fox jumped over the lazy dog"))
+ res = c.message(makeMessage("The cow jumped over the moon"))
+ res = c.message(makeMessage("The little dog laughed to see such fun"))
+ res = c.message(makeMessage("tl;dr"))
+ res = c.message(makeMessage("tl;dr"))
+ assert.True(t, res)
+ assert.Len(t, mb.Messages, 2)
+ assert.Contains(t, mb.Messages[1], "Slow down, cowboy.")
+}
+
+func TestAddHistoryLimitsMessages(t *testing.T) {
+ c, _ := setup(t)
+ max := 1000
+ c.bot.Config().Set("TLDR.HistorySize", strconv.Itoa(max))
+ c.bot.Config().Set("TLDR.KeepHours", "24")
+ t0 := time.Now().Add(-24 * time.Hour)
+ for i := 0; i < max*2; i++ {
+ hist := history{
+ body: "test",
+ user: "tester",
+ timestamp: t0.Add(time.Duration(i) * time.Second),
+ }
+ c.addHistory(hist)
+ }
+ assert.Len(t, c.history, max)
+}
+
+func TestAddHistoryLimitsDays(t *testing.T) {
+ c, _ := setup(t)
+ hrs := 24
+ expected := 24
+ c.bot.Config().Set("TLDR.HistorySize", "100")
+ c.bot.Config().Set("TLDR.KeepHours", strconv.Itoa(hrs))
+ t0 := time.Now().Add(-time.Duration(hrs*2) * time.Hour)
+ for i := 0; i < 48; i++ {
+ hist := history{
+ body: "test",
+ user: "tester",
+ timestamp: t0.Add(time.Duration(i) * time.Hour),
+ }
+ c.addHistory(hist)
+ }
+ assert.Len(t, c.history, expected, "%d != %d", len(c.history), expected)
+}
diff --git a/plugins/twitch/twitch.go b/plugins/twitch/twitch.go
index 941b55a..155fa9f 100644
--- a/plugins/twitch/twitch.go
+++ b/plugins/twitch/twitch.go
@@ -1,30 +1,37 @@
package twitch
import (
+ "bytes"
"encoding/json"
"fmt"
- "html/template"
"io/ioutil"
- "log"
"net/http"
"net/url"
"strings"
+ "text/template"
"time"
+ "github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
)
+const (
+ isStreamingTplFallback = "{{.Name}} is streaming {{.Game}} at {{.URL}}"
+ notStreamingTplFallback = "{{.Name}} is not streaming"
+ stoppedStreamingTplFallback = "{{.Name}} just stopped streaming"
+)
+
type TwitchPlugin struct {
- Bot bot.Bot
+ bot bot.Bot
config *config.Config
twitchList map[string]*Twitcher
}
type Twitcher struct {
- name string
- game string
+ name string
+ gameID string
}
func (t Twitcher) URL() string {
@@ -51,39 +58,34 @@ type stream struct {
} `json:"pagination"`
}
-func New(bot bot.Bot) *TwitchPlugin {
+func New(b bot.Bot) *TwitchPlugin {
p := &TwitchPlugin{
- Bot: bot,
- config: bot.Config(),
+ bot: b,
+ config: b.Config(),
twitchList: map[string]*Twitcher{},
}
- for _, users := range p.config.Twitch.Users {
- for _, twitcherName := range users {
+ for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) {
+ for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) {
if _, ok := p.twitchList[twitcherName]; !ok {
p.twitchList[twitcherName] = &Twitcher{
- name: twitcherName,
- game: "",
+ name: twitcherName,
+ gameID: "",
}
}
}
+ go p.twitchLoop(b.DefaultConnector(), ch)
}
- for channel := range p.config.Twitch.Users {
- go p.twitchLoop(channel)
- }
+ b.Register(p, bot.Message, p.message)
+ b.Register(p, bot.Help, p.help)
+ p.registerWeb()
return p
}
-func (p *TwitchPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-func (p *TwitchPlugin) RegisterWeb() *string {
+func (p *TwitchPlugin) registerWeb() {
http.HandleFunc("/isstreaming/", p.serveStreaming)
- tmp := "/isstreaming"
- return &tmp
}
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
@@ -101,61 +103,68 @@ func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
}
status := "NO."
- if twitcher.game != "" {
+ if twitcher.gameID != "" {
status = "YES."
}
context := map[string]interface{}{"Name": twitcher.name, "Status": status}
t, err := template.New("streaming").Parse(page)
if err != nil {
- log.Println("Could not parse template!", err)
+ log.Error().Err(err).Msg("Could not parse template!")
return
}
err = t.Execute(w, context)
if err != nil {
- log.Println("Could not execute template!", err)
+ log.Error().Err(err).Msg("Could not execute template!")
}
}
-func (p *TwitchPlugin) Message(message msg.Message) bool {
- if strings.ToLower(message.Body) == "twitch status" {
+func (p *TwitchPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ body := strings.ToLower(message.Body)
+ if body == "twitch status" {
channel := message.Channel
- if _, ok := p.config.Twitch.Users[channel]; ok {
- for _, twitcherName := range p.config.Twitch.Users[channel] {
- if _, ok = p.twitchList[twitcherName]; ok {
- p.checkTwitch(channel, p.twitchList[twitcherName], true)
+ if users := p.config.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
+ for _, twitcherName := range users {
+ if _, ok := p.twitchList[twitcherName]; ok {
+ p.checkTwitch(c, channel, p.twitchList[twitcherName], true)
}
}
}
return true
+ } else if body == "reset twitch" {
+ p.config.Set("twitch.istpl", isStreamingTplFallback)
+ p.config.Set("twitch.nottpl", notStreamingTplFallback)
+ p.config.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
}
return false
}
-func (p *TwitchPlugin) Event(kind string, message msg.Message) bool {
- return false
+func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ msg := "You can set the templates for streams with\n"
+ msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback)
+ msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback)
+ msg += fmt.Sprintf("twitch.stoppedtpl (default: %s)\n", stoppedStreamingTplFallback)
+ msg += "You can reset all messages with `!reset twitch`"
+ msg += "And you can ask who is streaming with `!twitch status`"
+ p.bot.Send(c, bot.Message, message.Channel, msg)
+ return true
}
-func (p *TwitchPlugin) LoadData() {
+func (p *TwitchPlugin) twitchLoop(c bot.Connector, channel string) {
+ frequency := p.config.GetInt("Twitch.Freq", 60)
+ if p.config.Get("twitch.clientid", "") == "" || p.config.Get("twitch.authorization", "") == "" {
+ log.Info().Msgf("Disabling twitch autochecking.")
+ return
+ }
-}
-
-func (p *TwitchPlugin) Help(channel string, parts []string) {
- msg := "There's no help for you here."
- p.Bot.SendMessage(channel, msg)
-}
-
-func (p *TwitchPlugin) twitchLoop(channel string) {
- frequency := p.config.Twitch.Freq
-
- log.Println("Checking every ", frequency, " seconds")
+ log.Info().Msgf("Checking every %d seconds", frequency)
for {
time.Sleep(time.Duration(frequency) * time.Second)
- for _, twitcherName := range p.config.Twitch.Users[channel] {
- p.checkTwitch(channel, p.twitchList[twitcherName], false)
+ for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) {
+ p.checkTwitch(c, channel, p.twitchList[twitcherName], false)
}
}
}
@@ -184,14 +193,14 @@ func getRequest(url, clientID, authorization string) ([]byte, bool) {
return body, true
errCase:
- log.Println(err)
+ log.Error().Err(err)
return []byte{}, false
}
-func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPrintStatus bool) {
+func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) {
baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil {
- log.Println("Error parsing twitch stream URL")
+ log.Error().Msg("Error parsing twitch stream URL")
return
}
@@ -200,8 +209,12 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
baseURL.RawQuery = query.Encode()
- cid := p.config.Twitch.ClientID
- auth := p.config.Twitch.Authorization
+ cid := p.config.Get("Twitch.ClientID", "")
+ auth := p.config.Get("Twitch.Authorization", "")
+ if cid == auth && cid == "" {
+ log.Info().Msgf("Twitch plugin not enabled.")
+ return
+ }
body, ok := getRequest(baseURL.String(), cid, auth)
if !ok {
@@ -211,32 +224,75 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
var s stream
err = json.Unmarshal(body, &s)
if err != nil {
- log.Println(err)
+ log.Error().Err(err)
return
}
games := s.Data
- game := ""
+ gameID, title := "", ""
if len(games) > 0 {
- game = games[0].Title
+ gameID = games[0].GameID
+ title = games[0].Title
}
+
+ notStreamingTpl := p.config.Get("Twitch.NotTpl", notStreamingTplFallback)
+ isStreamingTpl := p.config.Get("Twitch.IsTpl", isStreamingTplFallback)
+ stoppedStreamingTpl := p.config.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
+ buf := bytes.Buffer{}
+
+ info := struct {
+ Name string
+ Game string
+ URL string
+ }{
+ twitcher.name,
+ title,
+ twitcher.URL(),
+ }
+
if alwaysPrintStatus {
- if game == "" {
- p.Bot.SendMessage(channel, twitcher.name+" is not streaming.")
+ if gameID == "" {
+ t, err := template.New("notStreaming").Parse(notStreamingTpl)
+ if err != nil {
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, channel, err)
+ t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
+ }
+ t.Execute(&buf, info)
+ p.bot.Send(c, bot.Message, channel, buf.String())
} else {
- p.Bot.SendMessage(channel, twitcher.name+" is streaming "+game+" at "+twitcher.URL())
+ t, err := template.New("isStreaming").Parse(isStreamingTpl)
+ if err != nil {
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, channel, err)
+ t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
+ }
+ t.Execute(&buf, info)
+ p.bot.Send(c, bot.Message, channel, buf.String())
}
- } else if game == "" {
- if twitcher.game != "" {
- p.Bot.SendMessage(channel, twitcher.name+" just stopped streaming.")
+ } else if gameID == "" {
+ if twitcher.gameID != "" {
+ t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl)
+ if err != nil {
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, channel, err)
+ t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback))
+ }
+ t.Execute(&buf, info)
+ p.bot.Send(c, bot.Message, channel, buf.String())
}
- twitcher.game = ""
+ twitcher.gameID = ""
} else {
- if twitcher.game != game {
- p.Bot.SendMessage(channel, twitcher.name+" just started streaming "+game+" at "+twitcher.URL())
+ if twitcher.gameID != gameID {
+ t, err := template.New("isStreaming").Parse(isStreamingTpl)
+ if err != nil {
+ log.Error().Err(err)
+ p.bot.Send(c, bot.Message, channel, err)
+ t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
+ }
+ t.Execute(&buf, info)
+ p.bot.Send(c, bot.Message, channel, buf.String())
}
- twitcher.game = game
+ twitcher.gameID = gameID
}
}
-
-func (p *TwitchPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/twitch/twitch_test.go b/plugins/twitch/twitch_test.go
index ba0967d..1defec5 100644
--- a/plugins/twitch/twitch_test.go
+++ b/plugins/twitch/twitch_test.go
@@ -3,6 +3,7 @@
package twitch
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -28,12 +29,15 @@ func makeMessage(payload string) msg.Message {
func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
c := New(mb)
- c.config.Twitch.Users = map[string][]string{"test": []string{"drseabass"}}
+ mb.Config().Set("twitch.clientid", "fake")
+ mb.Config().Set("twitch.authorization", "fake")
+ c.config.SetArray("Twitch.Channels", []string{"test"})
+ c.config.SetArray("Twitch.test.Users", []string{"drseabass"})
assert.NotNil(t, c)
c.twitchList["drseabass"] = &Twitcher{
- name: "drseabass",
- game: "",
+ name: "drseabass",
+ gameID: "",
}
return c, mb
@@ -41,6 +45,6 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
func TestTwitch(t *testing.T) {
b, mb := makeTwitchPlugin(t)
- b.Message(makeMessage("!twitch status"))
+ b.message(makeMessage("!twitch status"))
assert.NotEmpty(t, mb.Messages)
}
diff --git a/plugins/your/your.go b/plugins/your/your.go
index 810b6fe..10dfc2b 100644
--- a/plugins/your/your.go
+++ b/plugins/your/your.go
@@ -17,52 +17,43 @@ type YourPlugin struct {
}
// NewYourPlugin creates a new YourPlugin with the Plugin interface
-func New(bot bot.Bot) *YourPlugin {
- return &YourPlugin{
- bot: bot,
- config: bot.Config(),
+func New(b bot.Bot) *YourPlugin {
+ yp := &YourPlugin{
+ bot: b,
+ config: b.Config(),
}
+ b.Register(yp, bot.Message, yp.message)
+ b.Register(yp, bot.Help, yp.help)
+ return yp
}
// 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 *YourPlugin) Message(message msg.Message) bool {
- if len(message.Body) > p.config.Your.MaxLength {
+func (p *YourPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ maxLen := p.config.GetInt("your.maxlength", 140)
+ if len(message.Body) > maxLen {
return false
}
msg := message.Body
- for _, replacement := range p.config.Your.Replacements {
- if rand.Float64() < replacement.Frequency {
- r := strings.NewReplacer(replacement.This, replacement.That)
+ for _, replacement := range p.config.GetArray("Your.Replacements", []string{}) {
+ freq := p.config.GetFloat64("your.replacements."+replacement+".freq", 0.0)
+ this := p.config.Get("your.replacements."+replacement+".this", "")
+ that := p.config.Get("your.replacements."+replacement+".that", "")
+ if rand.Float64() < freq {
+ r := strings.NewReplacer(this, that)
msg = r.Replace(msg)
}
}
if msg != message.Body {
- p.bot.SendMessage(message.Channel, msg)
+ p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
return false
}
// Help responds to help requests. Every plugin must implement a help function.
-func (p *YourPlugin) Help(channel string, parts []string) {
- p.bot.SendMessage(channel, "Your corrects people's grammar.")
+func (p *YourPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Your corrects people's grammar.")
+ return true
}
-
-// Empty event handler because this plugin does not do anything on event recv
-func (p *YourPlugin) Event(kind string, message msg.Message) bool {
- return false
-}
-
-// Handler for bot's own messages
-func (p *YourPlugin) BotMessage(message msg.Message) bool {
- return false
-}
-
-// Register any web URLs desired
-func (p *YourPlugin) RegisterWeb() *string {
- return nil
-}
-
-func (p *YourPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/plugins/your/your_test.go b/plugins/your/your_test.go
index 01d3f4f..ea13638 100644
--- a/plugins/your/your_test.go
+++ b/plugins/your/your_test.go
@@ -3,6 +3,7 @@
package your
import (
+ "github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@@ -10,15 +11,14 @@ import (
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
- "github.com/velour/catbase/config"
)
-func makeMessage(payload string) msg.Message {
+func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
- return msg.Message{
+ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@@ -26,46 +26,41 @@ func makeMessage(payload string) msg.Message {
}
}
-func TestReplacement(t *testing.T) {
+func setup(t *testing.T) (*YourPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
c := New(mb)
- assert.NotNil(t, c)
- c.config.Your.MaxLength = 1000
- c.config.Your.Replacements = []config.Replacement{
- config.Replacement{
- This: "fuck",
- That: "duck",
- Frequency: 1.0,
- },
- }
- res := c.Message(makeMessage("fuck a duck"))
- assert.Len(t, mb.Messages, 1)
+ mb.DB().MustExec(`delete from config;`)
+ return c, mb
+}
+
+func TestReplacement(t *testing.T) {
+ c, mb := setup(t)
+ c.config.Set("Your.MaxLength", "1000")
+ c.config.SetArray("your.replacements", []string{"0"})
+ c.config.Set("your.replacements.0.freq", "1.0")
+ c.config.Set("your.replacements.0.this", "fuck")
+ c.config.Set("your.replacements.0.that", "duck")
+ res := c.message(makeMessage("fuck a duck"))
assert.True(t, res)
+ assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "duck a duck")
}
func TestNoReplacement(t *testing.T) {
- mb := bot.NewMockBot()
- c := New(mb)
- assert.NotNil(t, c)
- c.config.Your.MaxLength = 1000
- c.config.Your.Replacements = []config.Replacement{
- config.Replacement{
- This: "nope",
- That: "duck",
- Frequency: 1.0,
- },
- config.Replacement{
- This: " fuck",
- That: "duck",
- Frequency: 1.0,
- },
- config.Replacement{
- This: "Fuck",
- That: "duck",
- Frequency: 1.0,
- },
- }
- c.Message(makeMessage("fuck a duck"))
+ c, mb := setup(t)
+ c.config.Set("Your.MaxLength", "1000")
+ c.config.SetArray("your.replacements", []string{"0", "1", "2"})
+ c.config.Set("your.replacements.0.freq", "1.0")
+ c.config.Set("your.replacements.0.this", "nope")
+ c.config.Set("your.replacements.0.that", "duck")
+
+ c.config.Set("your.replacements.1.freq", "1.0")
+ c.config.Set("your.replacements.1.this", "nope")
+ c.config.Set("your.replacements.1.that", "duck")
+
+ c.config.Set("your.replacements.2.freq", "1.0")
+ c.config.Set("your.replacements.2.this", "Fuck")
+ c.config.Set("your.replacements.2.that", "duck")
+ c.message(makeMessage("fuck a duck"))
assert.Len(t, mb.Messages, 0)
}
diff --git a/plugins/zork/zork.go b/plugins/zork/zork.go
index 7e5d0af..11458a7 100644
--- a/plugins/zork/zork.go
+++ b/plugins/zork/zork.go
@@ -8,12 +8,13 @@ import (
"bytes"
"go/build"
"io"
- "log"
"os/exec"
"path/filepath"
"strings"
"sync"
+ "github.com/rs/zerolog/log"
+
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@@ -26,14 +27,17 @@ type ZorkPlugin struct {
zorks map[string]io.WriteCloser
}
-func New(b bot.Bot) bot.Handler {
- return &ZorkPlugin{
+func New(b bot.Bot) bot.Plugin {
+ z := &ZorkPlugin{
bot: b,
zorks: make(map[string]io.WriteCloser),
}
+ b.Register(z, bot.Message, z.message)
+ b.Register(z, bot.Help, z.help)
+ return z
}
-func (p *ZorkPlugin) runZork(ch string) error {
+func (p *ZorkPlugin) runZork(c bot.Connector, ch string) error {
const importString = "github.com/velour/catbase/plugins/zork"
pkg, err := build.Import(importString, "", build.FindOnly)
if err != nil {
@@ -49,7 +53,7 @@ func (p *ZorkPlugin) runZork(ch string) error {
var w io.WriteCloser
cmd.Stdin, w = io.Pipe()
- log.Printf("zork running %v\n", cmd)
+ log.Info().Msgf("zork running %v", cmd)
if err := cmd.Start(); err != nil {
w.Close()
return err
@@ -75,25 +79,25 @@ func (p *ZorkPlugin) runZork(ch string) error {
m := strings.Replace(s.Text(), ">", "", -1)
m = strings.Replace(m, "\n", "\n>", -1)
m = ">" + m + "\n"
- p.bot.SendMessage(ch, m)
+ p.bot.Send(c, bot.Message, ch, m)
}
}()
go func() {
if err := cmd.Wait(); err != nil {
- log.Printf("zork exited: %v\n", err)
+ log.Error().Err(err).Msg("zork exited")
}
p.Lock()
p.zorks[ch] = nil
p.Unlock()
}()
- log.Printf("zork is running in %s\n", ch)
+ log.Info().Msgf("zork is running in %s\n", ch)
p.zorks[ch] = w
return nil
}
-func (p *ZorkPlugin) Message(message msg.Message) bool {
+func (p *ZorkPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
m := strings.ToLower(message.Body)
- log.Printf("got message [%s]\n", m)
+ log.Debug().Msgf("got message [%s]", m)
if ts := strings.Fields(m); len(ts) < 1 || ts[0] != "zork" {
return false
}
@@ -103,24 +107,17 @@ func (p *ZorkPlugin) Message(message msg.Message) bool {
p.Lock()
defer p.Unlock()
if p.zorks[ch] == nil {
- if err := p.runZork(ch); err != nil {
- p.bot.SendMessage(ch, "failed to run zork: "+err.Error())
+ if err := p.runZork(c, ch); err != nil {
+ p.bot.Send(c, bot.Message, ch, "failed to run zork: "+err.Error())
return true
}
}
- log.Printf("zorking, [%s]\n", m)
+ log.Debug().Msgf("zorking, [%s]", m)
io.WriteString(p.zorks[ch], m+"\n")
return true
}
-func (p *ZorkPlugin) Event(_ string, _ msg.Message) bool { return false }
-
-func (p *ZorkPlugin) BotMessage(_ msg.Message) bool { return false }
-
-func (p *ZorkPlugin) Help(ch string, _ []string) {
- p.bot.SendMessage(ch, "Play zork using 'zork '.")
+func (p *ZorkPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
+ p.bot.Send(c, bot.Message, message.Channel, "Play zork using 'zork '.")
+ return true
}
-
-func (p *ZorkPlugin) RegisterWeb() *string { return nil }
-
-func (p *ZorkPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
diff --git a/util/emojy/main.go b/util/emojy/main.go
index b3aea9c..03c421c 100644
--- a/util/emojy/main.go
+++ b/util/emojy/main.go
@@ -5,12 +5,14 @@ import (
"flag"
"io"
"io/ioutil"
- "log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
)
var (
@@ -20,9 +22,10 @@ var (
func main() {
flag.Parse()
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
if *token == "" {
- log.Printf("No token provided.")
+ log.Fatal().Msg("No token provided.")
return
}
@@ -35,7 +38,7 @@ func main() {
func getFiles() map[string]string {
files := fileResp{}
- log.Printf("Getting files")
+ log.Debug().Msgf("Getting files")
body := mkReq("https://slack.com/api/emoji.list",
"token", *token,
)
@@ -43,9 +46,9 @@ func getFiles() map[string]string {
err := json.Unmarshal(body, &files)
checkErr(err)
- log.Printf("Ok: %v", files.Ok)
+ log.Debug().Msgf("Ok: %v", files.Ok)
if !files.Ok {
- log.Println(files)
+ log.Debug().Msgf("%+v", files)
}
return files.Files
@@ -55,7 +58,7 @@ func downloadFile(n, f string) {
url := strings.Replace(f, "\\", "", -1) // because fuck slack
if strings.HasPrefix(url, "alias:") {
- log.Printf("Skipping alias: %s", url)
+ log.Debug().Msgf("Skipping alias: %s", url)
return
}
@@ -66,7 +69,7 @@ func downloadFile(n, f string) {
fname := filepath.Join(*path, n+"."+ext)
- log.Printf("Downloading from: %s", url)
+ log.Debug().Msgf("Downloading from: %s", url)
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
@@ -82,18 +85,18 @@ func downloadFile(n, f string) {
defer out.Close()
io.Copy(out, resp.Body)
- log.Printf("Downloaded %s", f)
+ log.Debug().Msgf("Downloaded %s", f)
}
func checkErr(err error) {
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
}
func mkReq(path string, arg ...string) []byte {
if len(arg)%2 != 0 {
- log.Fatal("Bad request arg number.")
+ log.Fatal().Msg("Bad request arg number.")
}
u, err := url.Parse(path)
diff --git a/util/files/main.go b/util/files/main.go
index 267c469..b7a87c3 100644
--- a/util/files/main.go
+++ b/util/files/main.go
@@ -5,13 +5,15 @@ import (
"flag"
"io"
"io/ioutil"
- "log"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
+
+ "github.com/rs/zerolog"
+ "github.com/rs/zerolog/log"
)
var (
@@ -24,6 +26,7 @@ var (
func main() {
flag.Parse()
+ log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
for {
files, count := getFiles()
@@ -40,7 +43,7 @@ func main() {
func getFiles() ([]slackFile, int) {
files := fileResp{}
- log.Printf("Getting files")
+ log.Debug().Msg("Getting files")
body := mkReq("https://slack.com/api/files.list",
"token", *token,
"count", strconv.Itoa(*limit),
@@ -50,9 +53,11 @@ func getFiles() ([]slackFile, int) {
err := json.Unmarshal(body, &files)
checkErr(err)
- log.Printf("Ok: %v, Count: %d", files.Ok, files.Paging.Count)
+ log.Info().
+ Int("count", files.Paging.Count).
+ Bool("ok", files.Ok)
if !files.Ok {
- log.Println(files)
+ log.Error().Interface("files", files)
}
return files.Files, files.Paging.Pages
@@ -69,18 +74,24 @@ func deleteFile(f slackFile) {
checkErr(err)
if !del.Ok {
- log.Println(body)
- log.Fatal("Couldn't delete " + f.ID)
+ log.Fatal().
+ Bytes("body", body).
+ Str("id", f.ID).
+ Msg("Couldn't delete")
}
- log.Printf("Deleted %s", f.ID)
+ log.Info().
+ Str("id", f.ID).
+ Msg("Deleted")
}
func downloadFile(f slackFile) {
url := strings.Replace(f.URLPrivateDownload, "\\", "", -1) // because fuck slack
fname := filepath.Join(*path, f.ID+f.Name)
- log.Printf("Downloading from: %s", url)
+ log.Info().
+ Str("url", url).
+ Msg("Downloading")
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
@@ -96,18 +107,20 @@ func downloadFile(f slackFile) {
defer out.Close()
io.Copy(out, resp.Body)
- log.Printf("Downloaded %s", f.ID)
+ log.Info().
+ Str("id", f.ID).
+ Msg("Downloaded")
}
func checkErr(err error) {
if err != nil {
- log.Fatal(err)
+ log.Fatal().Err(err)
}
}
func mkReq(path string, arg ...string) []byte {
if len(arg)%2 != 0 {
- log.Fatal("Bad request arg number.")
+ log.Fatal().Msg("Bad request arg number.")
}
u, err := url.Parse(path)
diff --git a/version.go b/version.go
deleted file mode 100644
index c2d9bee..0000000
--- a/version.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
-
-package main
-
-const Version = "0.9"