From ef40d335eb780971f213887a252d6af18ff35f49 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Wed, 30 Mar 2016 10:00:20 -0400 Subject: [PATCH] Make testing great again! Add examples in counter * Made bot.Bot an interface and added a mock with an in-memory database for plugins to use. * Remove logger nonsense * Rename Counter New --- bot/bot.go | 89 +++++++++-------- bot/handlers.go | 50 +++++----- bot/interfaces.go | 19 ++++ bot/mock.go | 48 +++++++++ bot/users.go | 10 +- main.go | 20 ++-- plugins/admin/admin.go | 6 +- plugins/beers/beers.go | 18 ++-- plugins/counter/counter.go | 19 ++-- plugins/counter/counter_test.go | 168 ++++++++++++++++++++++++++++++++ plugins/dice/dice.go | 4 +- plugins/downtime/downtime.go | 12 +-- plugins/fact/factoid.go | 18 ++-- plugins/fact/remember.go | 10 +- plugins/first/first.go | 18 ++-- plugins/leftpad/leftpad.go | 4 +- plugins/plugins.go | 94 ------------------ plugins/skeleton.go | 53 ---------- plugins/talker/talker.go | 10 +- plugins/your/your.go | 6 +- 20 files changed, 384 insertions(+), 292 deletions(-) create mode 100644 bot/mock.go create mode 100644 plugins/counter/counter_test.go delete mode 100644 plugins/skeleton.go diff --git a/bot/bot.go b/bot/bot.go index cf5613d..2a33cb5 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -16,33 +16,33 @@ import ( "github.com/velour/catbase/config" ) -// Bot type provides storage for bot-wide information, configs, and database connections -type Bot struct { +// bot type provides storage for bot-wide information, configs, and database connections +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 - PluginOrdering []string + plugins map[string]Handler + pluginOrdering []string // Users holds information about all of our friends - Users []User + users []User // Represents the bot - Me User + me User - Config *config.Config + config *config.Config - Conn Connector + 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 + db *sqlx.DB + dbVersion int64 logIn chan Message logOut chan Messages - Version string + version string // The entries to the bot's HTTP interface httpEndPoints map[string]string @@ -109,8 +109,8 @@ func init() { }) } -// NewBot creates a Bot for a given connection and set of handlers. -func NewBot(config *config.Config, connector Connector) *Bot { +// Newbot creates a bot for a given connection and set of handlers. +func New(config *config.Config, connector Connector) Bot { sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File) if err != nil { log.Fatal(err) @@ -127,17 +127,17 @@ func NewBot(config *config.Config, connector Connector) *Bot { }, } - bot := &Bot{ - Config: config, - Plugins: make(map[string]Handler), - PluginOrdering: make([]string, 0), - Conn: connector, - Users: users, - Me: users[0], - DB: sqlDB, + bot := &bot{ + config: config, + plugins: make(map[string]Handler), + pluginOrdering: make([]string, 0), + conn: connector, + users: users, + me: users[0], + db: sqlDB, logIn: logIn, logOut: logOut, - Version: config.Version, + version: config.Version, httpEndPoints: make(map[string]string), } @@ -155,32 +155,45 @@ func NewBot(config *config.Config, connector Connector) *Bot { return bot } +// 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 +} + // 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);`) +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) + 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) + 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)`) + _, err := b.db.Exec(`insert into version (version) values (1)`) if err != nil { log.Fatal("Initial DB migration insert: ", err) } } - if b.DBVersion == 1 { - if _, err := b.DB.Exec(`create table if not exists variables ( + if b.dbVersion == 1 { + if _, err := b.db.Exec(`create table if not exists variables ( id integer primary key, name string, perms string, @@ -188,7 +201,7 @@ func (b *Bot) migrateDB() { );`); err != nil { log.Fatal("Initial DB migration create variables table: ", err) } - if _, err := b.DB.Exec(`create table if not exists 'values' ( + if _, err := b.db.Exec(`create table if not exists 'values' ( id integer primary key, varId integer, value string @@ -199,18 +212,18 @@ func (b *Bot) migrateDB() { } // Adds a constructed handler to the bots handlers list -func (b *Bot) AddHandler(name string, h Handler) { - b.Plugins[strings.ToLower(name)] = h - b.PluginOrdering = append(b.PluginOrdering, name) +func (b *bot) AddHandler(name string, h Handler) { + b.plugins[strings.ToLower(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 { +func (b *bot) Who(channel string) []User { out := []User{} - for _, u := range b.Users { - if u.Name != b.Config.Nick { + for _, u := range b.users { + if u.Name != b.Config().Nick { out = append(out, u) } } @@ -247,7 +260,7 @@ var rootIndex string = ` ` -func (b *Bot) serveRoot(w http.ResponseWriter, r *http.Request) { +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) diff --git a/bot/handlers.go b/bot/handlers.go index 902fa7c..450f203 100644 --- a/bot/handlers.go +++ b/bot/handlers.go @@ -15,7 +15,7 @@ import ( ) // Handles incomming PRIVMSG requests -func (b *Bot) MsgReceived(msg Message) { +func (b *bot) MsgReceived(msg Message) { log.Println("Received message: ", msg) // msg := b.buildMessage(client, inMsg) @@ -27,8 +27,8 @@ func (b *Bot) MsgReceived(msg Message) { goto RET } - for _, name := range b.PluginOrdering { - p := b.Plugins[name] + for _, name := range b.pluginOrdering { + p := b.plugins[name] if p.Message(msg) { break } @@ -40,31 +40,31 @@ RET: } // Handle incoming events -func (b *Bot) EventReceived(msg Message) { +func (b *bot) EventReceived(msg Message) { log.Println("Received event: ", msg) //msg := b.buildMessage(conn, inMsg) - for _, name := range b.PluginOrdering { - p := b.Plugins[name] + 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) SendMessage(channel, message string) { - b.Conn.SendMessage(channel, message) +func (b *bot) SendMessage(channel, message string) { + b.conn.SendMessage(channel, message) } -func (b *Bot) SendAction(channel, message string) { - b.Conn.SendAction(channel, message) +func (b *bot) SendAction(channel, message string) { + b.conn.SendAction(channel, message) } // 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(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 { topics = fmt.Sprintf("%s, %s", topics, name) } b.SendMessage(channel, topics) @@ -78,7 +78,7 @@ func (b *Bot) checkHelp(channel string, parts []string) { b.listVars(channel, parts) return } - plugin := b.Plugins[parts[1]] + plugin := b.plugins[parts[1]] if plugin != nil { plugin.Help(channel, parts) } else { @@ -88,7 +88,7 @@ func (b *Bot) checkHelp(channel string, parts []string) { } } -func (b *Bot) LastMessage(channel string) (Message, error) { +func (b *bot) LastMessage(channel string) (Message, error) { log := <-b.logOut if len(log) == 0 { return Message{}, errors.New("No messages found.") @@ -103,7 +103,7 @@ func (b *Bot) LastMessage(channel string) (Message, error) { } // Take an input string and mutate it based on $vars in the string -func (b *Bot) Filter(message Message, input string) string { +func (b *bot) Filter(message Message, input string) string { rand.Seed(time.Now().Unix()) if strings.Contains(input, "$NICK") { @@ -155,9 +155,9 @@ func (b *Bot) Filter(message Message, input string) string { return input } -func (b *Bot) getVar(varName string) (string, error) { +func (b *bot) getVar(varName string) (string, error) { var text string - err := b.DB.QueryRow(`select v.value from variables as va inner join "values" as v on va.id = va.id = v.varId order by random() limit 1`).Scan(&text) + err := b.db.QueryRow(`select v.value from variables as va inner join "values" as v on va.id = va.id = v.varId order by random() limit 1`).Scan(&text) switch { case err == sql.ErrNoRows: return "", fmt.Errorf("No factoid found") @@ -167,8 +167,8 @@ func (b *Bot) getVar(varName string) (string, error) { return text, nil } -func (b *Bot) listVars(channel string, parts []string) { - rows, err := b.DB.Query(`select name from variables`) +func (b *bot) listVars(channel string, parts []string) { + rows, err := b.db.Query(`select name from variables`) if err != nil { log.Fatal(err) } @@ -185,17 +185,17 @@ func (b *Bot) listVars(channel string, parts []string) { b.SendMessage(channel, msg) } -func (b *Bot) Help(channel string, parts []string) { +func (b *bot) Help(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) + "http://github.com/velour/catbase", b.version) b.SendMessage(channel, msg) } // Send our own musings to the plugins -func (b *Bot) selfSaid(channel, message string, action bool) { +func (b *bot) selfSaid(channel, message string, action bool) { msg := Message{ - User: &b.Me, // hack + User: &b.me, // hack Channel: channel, Body: message, Raw: message, // hack @@ -205,8 +205,8 @@ func (b *Bot) selfSaid(channel, message string, action bool) { Host: "0.0.0.0", // hack } - for _, name := range b.PluginOrdering { - p := b.Plugins[name] + for _, name := range b.pluginOrdering { + p := b.plugins[name] if p.BotMessage(msg) { break } diff --git a/bot/interfaces.go b/bot/interfaces.go index 2a736bc..9d3054c 100644 --- a/bot/interfaces.go +++ b/bot/interfaces.go @@ -2,6 +2,25 @@ package bot +import ( + "github.com/jmoiron/sqlx" + "github.com/velour/catbase/config" +) + +type Bot interface { + Config() *config.Config + DBVersion() int64 + DB() *sqlx.DB + Who(string) []User + AddHandler(string, Handler) + SendMessage(string, string) + SendAction(string, string) + MsgReceived(Message) + EventReceived(Message) + Filter(Message, string) string + LastMessage(string) (Message, error) +} + type Connector interface { RegisterEventReceived(func(message Message)) RegisterMessageReceived(func(message Message)) diff --git a/bot/mock.go b/bot/mock.go new file mode 100644 index 0000000..a6748c2 --- /dev/null +++ b/bot/mock.go @@ -0,0 +1,48 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +package bot + +import ( + "log" + + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/mock" + "github.com/velour/catbase/config" +) + +type MockBot struct { + mock.Mock + db *sqlx.DB + + Messages []string + Actions []string +} + +func (mb *MockBot) Config() *config.Config { return &config.Config{} } +func (mb *MockBot) DBVersion() int64 { return 1 } +func (mb *MockBot) DB() *sqlx.DB { return mb.db } +func (mb *MockBot) Who(string) []User { return []User{} } +func (mb *MockBot) AddHandler(name string, f Handler) {} +func (mb *MockBot) SendMessage(ch string, msg string) { + mb.Messages = append(mb.Messages, msg) +} +func (mb *MockBot) SendAction(ch string, msg string) { + mb.Actions = append(mb.Actions, msg) +} +func (mb *MockBot) MsgReceived(msg Message) {} +func (mb *MockBot) EventReceived(msg Message) {} +func (mb *MockBot) Filter(msg Message, s string) string { return "" } +func (mb *MockBot) LastMessage(ch string) (Message, error) { return Message{}, nil } + +func NewMockBot() *MockBot { + db, err := sqlx.Open("sqlite3_custom", ":memory:") + if err != nil { + log.Fatal("Failed to open database:", err) + } + b := MockBot{ + db: db, + Messages: make([]string, 0), + Actions: make([]string, 0), + } + return &b +} diff --git a/bot/users.go b/bot/users.go index c442e78..ad8d6ff 100644 --- a/bot/users.go +++ b/bot/users.go @@ -16,12 +16,12 @@ type User struct { Admin bool - //bot *Bot + //bot *bot } var users = map[string]*User{} -func (b *Bot) GetUser(nick string) *User { +func (b *bot) GetUser(nick string) *User { if _, ok := users[nick]; !ok { users[nick] = &User{ Name: nick, @@ -31,15 +31,15 @@ func (b *Bot) GetUser(nick string) *User { return users[nick] } -func (b *Bot) NewUser(nick string) *User { +func (b *bot) NewUser(nick string) *User { return &User{ Name: nick, Admin: b.checkAdmin(nick), } } -func (b *Bot) checkAdmin(nick string) bool { - for _, u := range b.Config.Admins { +func (b *bot) checkAdmin(nick string) bool { + for _, u := range b.Config().Admins { if nick == u { return true } diff --git a/main.go b/main.go index 97b9960..f2b3db7 100644 --- a/main.go +++ b/main.go @@ -9,10 +9,8 @@ import ( "github.com/velour/catbase/bot" "github.com/velour/catbase/config" "github.com/velour/catbase/irc" - "github.com/velour/catbase/plugins" "github.com/velour/catbase/plugins/admin" "github.com/velour/catbase/plugins/beers" - "github.com/velour/catbase/plugins/counter" "github.com/velour/catbase/plugins/dice" "github.com/velour/catbase/plugins/downtime" "github.com/velour/catbase/plugins/fact" @@ -39,22 +37,20 @@ func main() { log.Fatalf("Unknown connection type: %s", c.Type) } - b := bot.NewBot(c, client) + b := bot.New(c, client) // b.AddHandler(plugins.NewTestPlugin(b)) - b.AddHandler("admin", admin.NewAdminPlugin(b)) + b.AddHandler("admin", admin.New(b)) // b.AddHandler("first", plugins.NewFirstPlugin(b)) b.AddHandler("leftpad", leftpad.New(b)) - b.AddHandler("downtime", downtime.NewDowntimePlugin(b)) + b.AddHandler("downtime", downtime.New(b)) b.AddHandler("talker", talker.New(b)) - b.AddHandler("dice", dice.NewDicePlugin(b)) - b.AddHandler("beers", beers.NewBeersPlugin(b)) - b.AddHandler("counter", counter.NewCounterPlugin(b)) - b.AddHandler("remember", fact.NewRememberPlugin(b)) - b.AddHandler("skeleton", plugins.NewSkeletonPlugin(b)) - b.AddHandler("your", your.NewYourPlugin(b)) + b.AddHandler("dice", dice.New(b)) + b.AddHandler("beers", beers.New(b)) + b.AddHandler("remember", fact.NewRemember(b)) + b.AddHandler("your", your.New(b)) // catches anything left, will always return true - b.AddHandler("factoid", fact.NewFactoidPlugin(b)) + b.AddHandler("factoid", fact.New(b)) client.Serve() } diff --git a/plugins/admin/admin.go b/plugins/admin/admin.go index f7f60d8..1cc8cb6 100644 --- a/plugins/admin/admin.go +++ b/plugins/admin/admin.go @@ -17,15 +17,15 @@ import ( // 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 } // NewAdminPlugin creates a new AdminPlugin with the Plugin interface -func NewAdminPlugin(bot *bot.Bot) *AdminPlugin { +func New(bot bot.Bot) *AdminPlugin { p := &AdminPlugin{ Bot: bot, - DB: bot.DB, + DB: bot.DB(), } p.LoadData() return p diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go index b71f079..3a39368 100644 --- a/plugins/beers/beers.go +++ b/plugins/beers/beers.go @@ -22,7 +22,7 @@ import ( // This is a skeleton plugin to serve as an example and quick copy/paste for new plugins. type BeersPlugin struct { - Bot *bot.Bot + Bot bot.Bot db *sqlx.DB } @@ -35,9 +35,9 @@ type untappdUser struct { } // NewBeersPlugin creates a new BeersPlugin with the Plugin interface -func NewBeersPlugin(bot *bot.Bot) *BeersPlugin { - if bot.DBVersion == 1 { - if _, err := bot.DB.Exec(`create table if not exists untappd ( +func New(bot bot.Bot) *BeersPlugin { + if bot.DBVersion() == 1 { + if _, err := bot.DB().Exec(`create table if not exists untappd ( id integer primary key, untappdUser string, channel string, @@ -49,10 +49,10 @@ func NewBeersPlugin(bot *bot.Bot) *BeersPlugin { } p := BeersPlugin{ Bot: bot, - db: bot.DB, + db: bot.DB(), } p.LoadData() - for _, channel := range bot.Config.Untappd.Channels { + for _, channel := range bot.Config().Untappd.Channels { go p.untappdLoop(channel) } return &p @@ -313,7 +313,7 @@ type Beers struct { } func (p *BeersPlugin) pullUntappd() ([]checkin, error) { - access_token := "?access_token=" + p.Bot.Config.Untappd.Token + access_token := "?access_token=" + p.Bot.Config().Untappd.Token baseUrl := "https://api.untappd.com/v4/checkin/recent/" url := baseUrl + access_token + "&limit=25" @@ -343,7 +343,7 @@ func (p *BeersPlugin) pullUntappd() ([]checkin, error) { } func (p *BeersPlugin) checkUntappd(channel string) { - token := p.Bot.Config.Untappd.Token + token := p.Bot.Config().Untappd.Token if token == "" || token == "" { log.Println("No Untappd token, cannot enable plugin.") return @@ -418,7 +418,7 @@ func (p *BeersPlugin) checkUntappd(channel string) { } func (p *BeersPlugin) untappdLoop(channel string) { - frequency := p.Bot.Config.Untappd.Freq + frequency := p.Bot.Config().Untappd.Freq log.Println("Checking every ", frequency, " seconds") diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index b823b1a..b929291 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -15,7 +15,7 @@ import ( // This is a counter plugin to count arbitrary things. type CounterPlugin struct { - Bot *bot.Bot + Bot bot.Bot DB *sqlx.DB } @@ -102,9 +102,9 @@ func (i *Item) Delete() error { } // NewCounterPlugin creates a new CounterPlugin with the Plugin interface -func NewCounterPlugin(bot *bot.Bot) *CounterPlugin { - if bot.DBVersion == 1 { - if _, err := bot.DB.Exec(`create table if not exists counter ( +func New(bot bot.Bot) *CounterPlugin { + if bot.DBVersion() == 1 { + if _, err := bot.DB().Exec(`create table if not exists counter ( id integer primary key, nick string, item string, @@ -115,7 +115,7 @@ func NewCounterPlugin(bot *bot.Bot) *CounterPlugin { } return &CounterPlugin{ Bot: bot, - DB: bot.DB, + DB: bot.DB(), } } @@ -156,7 +156,7 @@ func (p *CounterPlugin) Message(message bot.Message) bool { for _, it := range items { count += 1 if count > 1 { - resp += ", " + resp += "," } resp += fmt.Sprintf(" %s: %d", it.Item, it.Count) if count > 20 { @@ -271,13 +271,6 @@ func (p *CounterPlugin) Message(message bot.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 *CounterPlugin) LoadData() { - // This bot has no data to load -} - // 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 "+ diff --git a/plugins/counter/counter_test.go b/plugins/counter/counter_test.go new file mode 100644 index 0000000..7098e34 --- /dev/null +++ b/plugins/counter/counter_test.go @@ -0,0 +1,168 @@ +// © 2016 the CatBase Authors under the WTFPL license. See AUTHORS for the list of authors. + +package counter + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/velour/catbase/bot" +) + +func makeMessage(payload string) bot.Message { + isCmd := strings.HasPrefix(payload, "!") + if isCmd { + payload = payload[1:] + } + return bot.Message{ + User: &bot.User{Name: "tester"}, + Channel: "test", + Body: payload, + Command: isCmd, + } +} + +func TestCounterOne(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + c.Message(makeMessage("test++")) + assert.Len(t, mb.Messages, 1) + assert.Equal(t, mb.Messages[0], "tester has 1 test.") +} + +func TestCounterFour(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + c.Message(makeMessage("test++")) + } + assert.Len(t, mb.Messages, 4) + assert.Equal(t, mb.Messages[3], "tester has 4 test.") +} + +func TestCounterDecrement(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + c.Message(makeMessage("test++")) + assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1)) + } + c.Message(makeMessage("test--")) + assert.Len(t, mb.Messages, 5) + assert.Equal(t, mb.Messages[4], "tester has 3 test.") +} + +func TestFriendCounterDecrement(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + c.Message(makeMessage("other.test++")) + assert.Equal(t, mb.Messages[i], fmt.Sprintf("other has %d test.", i+1)) + } + c.Message(makeMessage("other.test--")) + assert.Len(t, mb.Messages, 5) + assert.Equal(t, mb.Messages[4], "other has 3 test.") +} + +func TestDecrementZero(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + 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--")) + assert.Equal(t, mb.Messages[j], fmt.Sprintf("tester has %d test.", i-1)) + j++ + } + assert.Len(t, mb.Messages, 8) + assert.Equal(t, mb.Messages[7], "tester has 0 test.") +} + +func TestClear(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + c.Message(makeMessage("test++")) + assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1)) + } + 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") +} + +func TestCount(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + c.Message(makeMessage("test++")) + assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1)) + } + 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.") +} + +func TestInspectMe(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + for i := 0; i < 4; i++ { + 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++")) + 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++")) + assert.Equal(t, mb.Messages[i+6], fmt.Sprintf("tester has %d cheese.", i+1)) + } + 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.") +} + +func TestHelp(t *testing.T) { + mb := bot.NewMockBot() + c := New(mb) + assert.NotNil(t, c) + c.Help("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/dice/dice.go b/plugins/dice/dice.go index d414933..5291b15 100644 --- a/plugins/dice/dice.go +++ b/plugins/dice/dice.go @@ -15,11 +15,11 @@ import ( // This is a dice plugin to serve as an example and quick copy/paste for new plugins. type DicePlugin struct { - Bot *bot.Bot + Bot bot.Bot } // NewDicePlugin creates a new DicePlugin with the Plugin interface -func NewDicePlugin(bot *bot.Bot) *DicePlugin { +func New(bot bot.Bot) *DicePlugin { return &DicePlugin{ Bot: bot, } diff --git a/plugins/downtime/downtime.go b/plugins/downtime/downtime.go index b5e9473..eddd84e 100644 --- a/plugins/downtime/downtime.go +++ b/plugins/downtime/downtime.go @@ -20,7 +20,7 @@ import ( // This is a downtime plugin to monitor how much our users suck type DowntimePlugin struct { - Bot *bot.Bot + Bot bot.Bot db *sqlx.DB } @@ -100,13 +100,13 @@ func (ie idleEntries) Swap(i, j int) { } // NewDowntimePlugin creates a new DowntimePlugin with the Plugin interface -func NewDowntimePlugin(bot *bot.Bot) *DowntimePlugin { +func New(bot bot.Bot) *DowntimePlugin { p := DowntimePlugin{ Bot: bot, - db: bot.DB, + db: bot.DB(), } - if bot.DBVersion == 1 { + if bot.DBVersion() == 1 { _, err := p.db.Exec(`create table if not exists downtime ( id integer primary key, nick string, @@ -160,7 +160,7 @@ func (p *DowntimePlugin) Message(message bot.Message) bool { for _, e := range entries { // filter out ZNC entries and ourself - if strings.HasPrefix(e.nick, "*") || strings.ToLower(p.Bot.Config.Nick) == e.nick { + 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)) @@ -204,7 +204,7 @@ func (p *DowntimePlugin) Help(channel string, parts []string) { // Empty event handler because this plugin does not do anything on event recv func (p *DowntimePlugin) Event(kind string, message bot.Message) bool { log.Println(kind, "\t", message) - if kind != "PART" && message.User.Name != p.Bot.Config.Nick { + 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)) diff --git a/plugins/fact/factoid.go b/plugins/fact/factoid.go index e7e10e3..6cda9fd 100644 --- a/plugins/fact/factoid.go +++ b/plugins/fact/factoid.go @@ -196,14 +196,14 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) { // FactoidPlugin provides the necessary plugin-wide needs type FactoidPlugin struct { - Bot *bot.Bot + Bot bot.Bot NotFound []string LastFact *factoid db *sqlx.DB } // NewFactoidPlugin creates a new FactoidPlugin with the Plugin interface -func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin { +func New(botInst bot.Bot) *FactoidPlugin { p := &FactoidPlugin{ Bot: botInst, NotFound: []string{ @@ -214,7 +214,7 @@ func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin { "NOPE! NOPE! NOPE!", "One time, I learned how to jump rope.", }, - db: botInst.DB, + db: botInst.DB(), } _, err := p.db.Exec(`create table if not exists factoid ( @@ -231,13 +231,13 @@ func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin { log.Fatal(err) } - for _, channel := range botInst.Config.Channels { + for _, channel := range botInst.Config().Channels { go p.factTimer(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.StartupFact); ok { + if ok, fact := p.findTrigger(p.Bot.Config().StartupFact); ok { p.sayFact(bot.Message{ Channel: ch, Body: "speed test", // BUG: This is defined in the config too @@ -596,7 +596,7 @@ func (p *FactoidPlugin) randomFact() *factoid { // factTimer spits out a fact at a given interval and with given probability func (p *FactoidPlugin) factTimer(channel string) { - duration := time.Duration(p.Bot.Config.QuoteTime) * time.Minute + duration := time.Duration(p.Bot.Config().QuoteTime) * time.Minute myLastMsg := time.Now() for { time.Sleep(time.Duration(5) * time.Second) @@ -609,7 +609,7 @@ func (p *FactoidPlugin) factTimer(channel string) { tdelta := time.Since(lastmsg.Time) earlier := time.Since(myLastMsg) > tdelta chance := rand.Float64() - success := chance < p.Bot.Config.QuoteChance + success := chance < p.Bot.Config().QuoteChance if success && tdelta > duration && earlier { fact := p.randomFact() @@ -617,9 +617,11 @@ func (p *FactoidPlugin) factTimer(channel string) { continue } + users := p.Bot.Who(channel) + // we need to fabricate a message so that bot.Filter can operate message := bot.Message{ - User: &p.Bot.Users[rand.Intn(len(p.Bot.Users))], + User: &users[rand.Intn(len(users))], Channel: channel, } p.sayFact(message, *fact) diff --git a/plugins/fact/remember.go b/plugins/fact/remember.go index 8a8bfbe..64af8c5 100644 --- a/plugins/fact/remember.go +++ b/plugins/fact/remember.go @@ -17,17 +17,17 @@ import ( // plugins. type RememberPlugin struct { - Bot *bot.Bot + Bot bot.Bot Log map[string][]bot.Message db *sqlx.DB } // NewRememberPlugin creates a new RememberPlugin with the Plugin interface -func NewRememberPlugin(b *bot.Bot) *RememberPlugin { +func NewRemember(b bot.Bot) *RememberPlugin { p := RememberPlugin{ Bot: b, Log: make(map[string][]bot.Message), - db: b.DB, + db: b.DB(), } return &p } @@ -167,8 +167,8 @@ func (p *RememberPlugin) quoteTimer(channel string) { for { // this pisses me off: You can't multiply int * time.Duration so it // has to look ugly as shit. - time.Sleep(time.Duration(p.Bot.Config.QuoteTime) * time.Minute) - chance := 1.0 / p.Bot.Config.QuoteChance + time.Sleep(time.Duration(p.Bot.Config().QuoteTime) * time.Minute) + chance := 1.0 / p.Bot.Config().QuoteChance if rand.Intn(int(chance)) == 0 { msg := p.randQuote() p.Bot.SendMessage(channel, msg) diff --git a/plugins/first/first.go b/plugins/first/first.go index 3bc43a7..39c663f 100644 --- a/plugins/first/first.go +++ b/plugins/first/first.go @@ -18,7 +18,7 @@ import ( type FirstPlugin struct { First *FirstEntry - Bot *bot.Bot + Bot bot.Bot db *sqlx.DB } @@ -46,9 +46,9 @@ func (fe *FirstEntry) save(db *sqlx.DB) error { } // NewFirstPlugin creates a new FirstPlugin with the Plugin interface -func NewFirstPlugin(b *bot.Bot) *FirstPlugin { - if b.DBVersion == 1 { - _, err := b.DB.Exec(`create table if not exists first ( +func New(b bot.Bot) *FirstPlugin { + if b.DBVersion() == 1 { + _, err := b.DB().Exec(`create table if not exists first ( id integer primary key, day integer, time integer, @@ -62,14 +62,14 @@ func NewFirstPlugin(b *bot.Bot) *FirstPlugin { log.Println("First plugin initialized with day:", midnight(time.Now())) - first, err := getLastFirst(b.DB) + first, err := getLastFirst(b.DB()) if err != nil { log.Fatal("Could not initialize first plugin: ", err) } return &FirstPlugin{ Bot: b, - db: b.DB, + db: b.DB(), First: first, } } @@ -151,7 +151,7 @@ func (p *FirstPlugin) Message(message bot.Message) bool { } func (p *FirstPlugin) allowed(message bot.Message) bool { - for _, msg := range p.Bot.Config.Bad.Msgs { + for _, msg := range p.Bot.Config().Bad.Msgs { match, err := regexp.MatchString(msg, strings.ToLower(message.Body)) if err != nil { log.Println("Bad regexp: ", err) @@ -161,13 +161,13 @@ func (p *FirstPlugin) allowed(message bot.Message) bool { return false } } - for _, host := range p.Bot.Config.Bad.Hosts { + for _, host := range p.Bot.Config().Bad.Hosts { if host == message.Host { log.Println("Disallowing first: ", message.User.Name, ":", message.Body) return false } } - for _, nick := range p.Bot.Config.Bad.Nicks { + for _, nick := range p.Bot.Config().Bad.Nicks { if nick == message.User.Name { log.Println("Disallowing first: ", message.User.Name, ":", message.Body) return false diff --git a/plugins/leftpad/leftpad.go b/plugins/leftpad/leftpad.go index 0a5a190..e1550be 100644 --- a/plugins/leftpad/leftpad.go +++ b/plugins/leftpad/leftpad.go @@ -15,11 +15,11 @@ import ( ) type LeftpadPlugin struct { - bot *bot.Bot + bot bot.Bot } // New creates a new LeftpadPlugin with the Plugin interface -func New(bot *bot.Bot) *LeftpadPlugin { +func New(bot bot.Bot) *LeftpadPlugin { p := LeftpadPlugin{ bot: bot, } diff --git a/plugins/plugins.go b/plugins/plugins.go index 375f13f..80c1d82 100644 --- a/plugins/plugins.go +++ b/plugins/plugins.go @@ -2,7 +2,6 @@ package plugins -import "fmt" import "github.com/velour/catbase/bot" // Plugin interface defines the methods needed to accept a plugin @@ -14,96 +13,3 @@ type Plugin interface { Help() RegisterWeb() } - -// ---- Below are some example plugins - -// Creates a new TestPlugin with the Plugin interface -func NewTestPlugin(bot *bot.Bot) *TestPlugin { - tp := TestPlugin{} - tp.LoadData() - tp.Bot = bot - return &tp -} - -// TestPlugin type allows our plugin to store persistent state information -type TestPlugin struct { - Bot *bot.Bot - Responds []string - Name string - Feces string - helpmsg []string -} - -func (p *TestPlugin) LoadData() { - config := GetPluginConfig("TestPlugin") - p.Name = config.Name - p.Feces = config.Values["Feces"].(string) - p.helpmsg = []string{ - "TestPlugin just shows off how shit works.", - } -} - -func (p *TestPlugin) Message(message bot.Message) bool { - user := message.User - channel := message.Channel - body := message.Body - - fmt.Println(user, body) - fmt.Println("My plugin name is:", p.Name, " My feces are:", p.Feces) - p.Bot.SendMessage(channel, body) - return true -} - -func (p *TestPlugin) Help(message bot.Message) { - for _, msg := range p.helpmsg { - p.Bot.SendMessage(message.Channel, msg) - } -} - -// Empty event handler because this plugin does not do anything on event recv -func (p *TestPlugin) Event(kind string, message bot.Message) bool { - return false -} - -// Handler for bot's own messages -func (p *TestPlugin) BotMessage(message bot.Message) bool { - return false -} - -type PluginConfig struct { - Name string - Values map[string]interface{} -} - -// Loads plugin config (could be out of a DB or something) -func GetPluginConfig(name string) PluginConfig { - return PluginConfig{ - Name: "TestPlugin", - Values: map[string]interface{}{ - "Feces": "test", - "Responds": "fucker", - }, - } -} - -// FalsePlugin shows how plugin fallthrough works for handling messages -type FalsePlugin struct{} - -func (fp FalsePlugin) Message(user, message string) bool { - fmt.Println("FalsePlugin returning false.") - return false -} - -func (fp FalsePlugin) LoadData() { - -} - -// Empty event handler because this plugin does not do anything on event recv -func (p *FalsePlugin) Event(kind string, message bot.Message) bool { - return false -} - -// Handler for bot's own messages -func (p *FalsePlugin) BotMessage(message bot.Message) bool { - return false -} diff --git a/plugins/skeleton.go b/plugins/skeleton.go deleted file mode 100644 index f30df1c..0000000 --- a/plugins/skeleton.go +++ /dev/null @@ -1,53 +0,0 @@ -// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. - -package plugins - -import "github.com/velour/catbase/bot" - -// This is a skeleton plugin to serve as an example and quick copy/paste for new plugins. - -type SkeletonPlugin struct { - Bot *bot.Bot -} - -// NewSkeletonPlugin creates a new SkeletonPlugin with the Plugin interface -func NewSkeletonPlugin(bot *bot.Bot) *SkeletonPlugin { - return &SkeletonPlugin{ - Bot: bot, - } -} - -// 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 *SkeletonPlugin) Message(message bot.Message) bool { - // This bot does not reply to anything - 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 *SkeletonPlugin) LoadData() { - // This bot has no data to load -} - -// Help responds to help requests. Every plugin must implement a help function. -func (p *SkeletonPlugin) Help(channel string, parts []string) { - p.Bot.SendMessage(channel, "Sorry, Skeleton does not do a goddamn thing.") -} - -// Empty event handler because this plugin does not do anything on event recv -func (p *SkeletonPlugin) Event(kind string, message bot.Message) bool { - return false -} - -// Handler for bot's own messages -func (p *SkeletonPlugin) BotMessage(message bot.Message) bool { - return false -} - -// Register any web URLs desired -func (p *SkeletonPlugin) RegisterWeb() *string { - return nil -} diff --git a/plugins/talker/talker.go b/plugins/talker/talker.go index ba3f79d..cfd3b88 100644 --- a/plugins/talker/talker.go +++ b/plugins/talker/talker.go @@ -40,14 +40,14 @@ var goatse []string = []string{ } type TalkerPlugin struct { - Bot *bot.Bot + Bot bot.Bot enforceNicks bool } -func New(bot *bot.Bot) *TalkerPlugin { +func New(bot bot.Bot) *TalkerPlugin { return &TalkerPlugin{ Bot: bot, - enforceNicks: bot.Config.EnforceNicks, + enforceNicks: bot.Config().EnforceNicks, } } @@ -105,11 +105,11 @@ func (p *TalkerPlugin) Help(channel string, parts []string) { // Empty event handler because this plugin does not do anything on event recv func (p *TalkerPlugin) Event(kind string, message bot.Message) bool { - sayings := p.Bot.Config.WelcomeMsgs + sayings := p.Bot.Config().WelcomeMsgs if len(sayings) == 0 { return false } - if kind == "JOIN" && strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config.Nick) { + if kind == "JOIN" && strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) { msg := fmt.Sprintf(sayings[rand.Intn(len(sayings))], message.User.Name) p.Bot.SendMessage(message.Channel, msg) return true diff --git a/plugins/your/your.go b/plugins/your/your.go index 2976828..e65270d 100644 --- a/plugins/your/your.go +++ b/plugins/your/your.go @@ -12,11 +12,11 @@ import ( ) type YourPlugin struct { - bot *bot.Bot + bot bot.Bot } // NewYourPlugin creates a new YourPlugin with the Plugin interface -func NewYourPlugin(bot *bot.Bot) *YourPlugin { +func New(bot bot.Bot) *YourPlugin { rand.Seed(time.Now().Unix()) return &YourPlugin{ bot: bot, @@ -28,7 +28,7 @@ func NewYourPlugin(bot *bot.Bot) *YourPlugin { // Otherwise, the function returns false and the bot continues execution of other plugins. func (p *YourPlugin) Message(message bot.Message) bool { lower := strings.ToLower(message.Body) - config := p.bot.Config.Your + config := p.bot.Config().Your if len(message.Body) > config.MaxLength { return false }