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
This commit is contained in:
Chris Sexton 2016-03-30 10:00:20 -04:00
parent a34afa97ad
commit ef40d335eb
20 changed files with 384 additions and 292 deletions

View File

@ -16,33 +16,33 @@ import (
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
) )
// Bot type provides storage for bot-wide information, configs, and database connections // bot type provides storage for bot-wide information, configs, and database connections
type Bot struct { type bot struct {
// Each plugin must be registered in our plugins handler. To come: a map so that this // 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 // will allow plugins to respond to specific kinds of events
Plugins map[string]Handler plugins map[string]Handler
PluginOrdering []string pluginOrdering []string
// Users holds information about all of our friends // Users holds information about all of our friends
Users []User users []User
// Represents the bot // Represents the bot
Me User me User
Config *config.Config config *config.Config
Conn Connector conn Connector
// SQL DB // SQL DB
// TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that // 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 // the select/update/etc statements could be simplified with struct
// marshalling. // marshalling.
DB *sqlx.DB db *sqlx.DB
DBVersion int64 dbVersion int64
logIn chan Message logIn chan Message
logOut chan Messages logOut chan Messages
Version string version string
// The entries to the bot's HTTP interface // The entries to the bot's HTTP interface
httpEndPoints map[string]string httpEndPoints map[string]string
@ -109,8 +109,8 @@ func init() {
}) })
} }
// NewBot creates a Bot for a given connection and set of handlers. // Newbot creates a bot for a given connection and set of handlers.
func NewBot(config *config.Config, connector Connector) *Bot { func New(config *config.Config, connector Connector) Bot {
sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File) sqlDB, err := sqlx.Open("sqlite3_custom", config.DB.File)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
@ -127,17 +127,17 @@ func NewBot(config *config.Config, connector Connector) *Bot {
}, },
} }
bot := &Bot{ bot := &bot{
Config: config, config: config,
Plugins: make(map[string]Handler), plugins: make(map[string]Handler),
PluginOrdering: make([]string, 0), pluginOrdering: make([]string, 0),
Conn: connector, conn: connector,
Users: users, users: users,
Me: users[0], me: users[0],
DB: sqlDB, db: sqlDB,
logIn: logIn, logIn: logIn,
logOut: logOut, logOut: logOut,
Version: config.Version, version: config.Version,
httpEndPoints: make(map[string]string), httpEndPoints: make(map[string]string),
} }
@ -155,32 +155,45 @@ func NewBot(config *config.Config, connector Connector) *Bot {
return 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 // Create any tables if necessary based on version of DB
// Plugins should create their own tables, these are only for official bot stuff // 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. // Note: This does not return an error. Database issues are all fatal at this stage.
func (b *Bot) migrateDB() { func (b *bot) migrateDB() {
_, err := b.DB.Exec(`create table if not exists version (version integer);`) _, err := b.db.Exec(`create table if not exists version (version integer);`)
if err != nil { if err != nil {
log.Fatal("Initial DB migration create version table: ", err) log.Fatal("Initial DB migration create version table: ", err)
} }
var version sql.NullInt64 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 { if err != nil {
log.Fatal("Initial DB migration get version: ", err) log.Fatal("Initial DB migration get version: ", err)
} }
if version.Valid { if version.Valid {
b.DBVersion = version.Int64 b.dbVersion = version.Int64
log.Printf("Database version: %v\n", b.DBVersion) log.Printf("Database version: %v\n", b.dbVersion)
} else { } else {
log.Printf("No versions, we're the first!.") 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 { if err != nil {
log.Fatal("Initial DB migration insert: ", err) log.Fatal("Initial DB migration insert: ", err)
} }
} }
if b.DBVersion == 1 { if b.dbVersion == 1 {
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, id integer primary key,
name string, name string,
perms string, perms string,
@ -188,7 +201,7 @@ func (b *Bot) migrateDB() {
);`); err != nil { );`); err != nil {
log.Fatal("Initial DB migration create variables table: ", err) 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, id integer primary key,
varId integer, varId integer,
value string value string
@ -199,18 +212,18 @@ func (b *Bot) migrateDB() {
} }
// Adds a constructed handler to the bots handlers list // Adds a constructed handler to the bots handlers list
func (b *Bot) AddHandler(name string, h Handler) { func (b *bot) AddHandler(name string, h Handler) {
b.Plugins[strings.ToLower(name)] = h b.plugins[strings.ToLower(name)] = h
b.PluginOrdering = append(b.PluginOrdering, name) b.pluginOrdering = append(b.pluginOrdering, name)
if entry := h.RegisterWeb(); entry != nil { if entry := h.RegisterWeb(); entry != nil {
b.httpEndPoints[name] = *entry b.httpEndPoints[name] = *entry
} }
} }
func (b *Bot) Who(channel string) []User { func (b *bot) Who(channel string) []User {
out := []User{} out := []User{}
for _, u := range b.Users { for _, u := range b.users {
if u.Name != b.Config.Nick { if u.Name != b.Config().Nick {
out = append(out, u) out = append(out, u)
} }
} }
@ -247,7 +260,7 @@ var rootIndex string = `
</html> </html>
` `
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 := make(map[string]interface{})
context["EndPoints"] = b.httpEndPoints context["EndPoints"] = b.httpEndPoints
t, err := template.New("rootIndex").Parse(rootIndex) t, err := template.New("rootIndex").Parse(rootIndex)

View File

@ -15,7 +15,7 @@ import (
) )
// Handles incomming PRIVMSG requests // Handles incomming PRIVMSG requests
func (b *Bot) MsgReceived(msg Message) { func (b *bot) MsgReceived(msg Message) {
log.Println("Received message: ", msg) log.Println("Received message: ", msg)
// msg := b.buildMessage(client, inMsg) // msg := b.buildMessage(client, inMsg)
@ -27,8 +27,8 @@ func (b *Bot) MsgReceived(msg Message) {
goto RET goto RET
} }
for _, name := range b.PluginOrdering { for _, name := range b.pluginOrdering {
p := b.Plugins[name] p := b.plugins[name]
if p.Message(msg) { if p.Message(msg) {
break break
} }
@ -40,31 +40,31 @@ RET:
} }
// Handle incoming events // Handle incoming events
func (b *Bot) EventReceived(msg Message) { func (b *bot) EventReceived(msg Message) {
log.Println("Received event: ", msg) log.Println("Received event: ", msg)
//msg := b.buildMessage(conn, inMsg) //msg := b.buildMessage(conn, inMsg)
for _, name := range b.PluginOrdering { for _, name := range b.pluginOrdering {
p := b.Plugins[name] p := b.plugins[name]
if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body
break break
} }
} }
} }
func (b *Bot) SendMessage(channel, message string) { func (b *bot) SendMessage(channel, message string) {
b.Conn.SendMessage(channel, message) b.conn.SendMessage(channel, message)
} }
func (b *Bot) SendAction(channel, message string) { func (b *bot) SendAction(channel, message string) {
b.Conn.SendAction(channel, message) b.conn.SendAction(channel, message)
} }
// Checks to see if the user is asking for help, returns true if so and handles the situation. // 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 { if len(parts) == 1 {
// just print out a list of help topics // just print out a list of help topics
topics := "Help topics: about variables" topics := "Help topics: about variables"
for name, _ := range b.Plugins { for name, _ := range b.plugins {
topics = fmt.Sprintf("%s, %s", topics, name) topics = fmt.Sprintf("%s, %s", topics, name)
} }
b.SendMessage(channel, topics) b.SendMessage(channel, topics)
@ -78,7 +78,7 @@ func (b *Bot) checkHelp(channel string, parts []string) {
b.listVars(channel, parts) b.listVars(channel, parts)
return return
} }
plugin := b.Plugins[parts[1]] plugin := b.plugins[parts[1]]
if plugin != nil { if plugin != nil {
plugin.Help(channel, parts) plugin.Help(channel, parts)
} else { } 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 log := <-b.logOut
if len(log) == 0 { if len(log) == 0 {
return Message{}, errors.New("No messages found.") 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 // 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()) rand.Seed(time.Now().Unix())
if strings.Contains(input, "$NICK") { if strings.Contains(input, "$NICK") {
@ -155,9 +155,9 @@ func (b *Bot) Filter(message Message, input string) string {
return input return input
} }
func (b *Bot) getVar(varName string) (string, error) { func (b *bot) getVar(varName string) (string, error) {
var text string 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 { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return "", fmt.Errorf("No factoid found") return "", fmt.Errorf("No factoid found")
@ -167,8 +167,8 @@ func (b *Bot) getVar(varName string) (string, error) {
return text, nil return text, nil
} }
func (b *Bot) listVars(channel string, parts []string) { func (b *bot) listVars(channel string, parts []string) {
rows, err := b.DB.Query(`select name from variables`) rows, err := b.db.Query(`select name from variables`)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -185,17 +185,17 @@ func (b *Bot) listVars(channel string, parts []string) {
b.SendMessage(channel, msg) 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 "+ 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: "+ "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) b.SendMessage(channel, msg)
} }
// Send our own musings to the plugins // 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{ msg := Message{
User: &b.Me, // hack User: &b.me, // hack
Channel: channel, Channel: channel,
Body: message, Body: message,
Raw: message, // hack Raw: message, // hack
@ -205,8 +205,8 @@ func (b *Bot) selfSaid(channel, message string, action bool) {
Host: "0.0.0.0", // hack Host: "0.0.0.0", // hack
} }
for _, name := range b.PluginOrdering { for _, name := range b.pluginOrdering {
p := b.Plugins[name] p := b.plugins[name]
if p.BotMessage(msg) { if p.BotMessage(msg) {
break break
} }

View File

@ -2,6 +2,25 @@
package bot 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 { type Connector interface {
RegisterEventReceived(func(message Message)) RegisterEventReceived(func(message Message))
RegisterMessageReceived(func(message Message)) RegisterMessageReceived(func(message Message))

48
bot/mock.go Normal file
View File

@ -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
}

View File

@ -16,12 +16,12 @@ type User struct {
Admin bool Admin bool
//bot *Bot //bot *bot
} }
var users = map[string]*User{} var users = map[string]*User{}
func (b *Bot) GetUser(nick string) *User { func (b *bot) GetUser(nick string) *User {
if _, ok := users[nick]; !ok { if _, ok := users[nick]; !ok {
users[nick] = &User{ users[nick] = &User{
Name: nick, Name: nick,
@ -31,15 +31,15 @@ func (b *Bot) GetUser(nick string) *User {
return users[nick] return users[nick]
} }
func (b *Bot) NewUser(nick string) *User { func (b *bot) NewUser(nick string) *User {
return &User{ return &User{
Name: nick, Name: nick,
Admin: b.checkAdmin(nick), Admin: b.checkAdmin(nick),
} }
} }
func (b *Bot) checkAdmin(nick string) bool { func (b *bot) checkAdmin(nick string) bool {
for _, u := range b.Config.Admins { for _, u := range b.Config().Admins {
if nick == u { if nick == u {
return true return true
} }

20
main.go
View File

@ -9,10 +9,8 @@ import (
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
"github.com/velour/catbase/irc" "github.com/velour/catbase/irc"
"github.com/velour/catbase/plugins"
"github.com/velour/catbase/plugins/admin" "github.com/velour/catbase/plugins/admin"
"github.com/velour/catbase/plugins/beers" "github.com/velour/catbase/plugins/beers"
"github.com/velour/catbase/plugins/counter"
"github.com/velour/catbase/plugins/dice" "github.com/velour/catbase/plugins/dice"
"github.com/velour/catbase/plugins/downtime" "github.com/velour/catbase/plugins/downtime"
"github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/fact"
@ -39,22 +37,20 @@ func main() {
log.Fatalf("Unknown connection type: %s", c.Type) 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(plugins.NewTestPlugin(b))
b.AddHandler("admin", admin.NewAdminPlugin(b)) b.AddHandler("admin", admin.New(b))
// b.AddHandler("first", plugins.NewFirstPlugin(b)) // b.AddHandler("first", plugins.NewFirstPlugin(b))
b.AddHandler("leftpad", leftpad.New(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("talker", talker.New(b))
b.AddHandler("dice", dice.NewDicePlugin(b)) b.AddHandler("dice", dice.New(b))
b.AddHandler("beers", beers.NewBeersPlugin(b)) b.AddHandler("beers", beers.New(b))
b.AddHandler("counter", counter.NewCounterPlugin(b)) b.AddHandler("remember", fact.NewRemember(b))
b.AddHandler("remember", fact.NewRememberPlugin(b)) b.AddHandler("your", your.New(b))
b.AddHandler("skeleton", plugins.NewSkeletonPlugin(b))
b.AddHandler("your", your.NewYourPlugin(b))
// catches anything left, will always return true // catches anything left, will always return true
b.AddHandler("factoid", fact.NewFactoidPlugin(b)) b.AddHandler("factoid", fact.New(b))
client.Serve() client.Serve()
} }

View File

@ -17,15 +17,15 @@ import (
// This is a admin plugin to serve as an example and quick copy/paste for new plugins. // This is a admin plugin to serve as an example and quick copy/paste for new plugins.
type AdminPlugin struct { type AdminPlugin struct {
Bot *bot.Bot Bot bot.Bot
DB *sqlx.DB DB *sqlx.DB
} }
// NewAdminPlugin creates a new AdminPlugin with the Plugin interface // NewAdminPlugin creates a new AdminPlugin with the Plugin interface
func NewAdminPlugin(bot *bot.Bot) *AdminPlugin { func New(bot bot.Bot) *AdminPlugin {
p := &AdminPlugin{ p := &AdminPlugin{
Bot: bot, Bot: bot,
DB: bot.DB, DB: bot.DB(),
} }
p.LoadData() p.LoadData()
return p return p

View File

@ -22,7 +22,7 @@ import (
// This is a skeleton plugin to serve as an example and quick copy/paste for new plugins. // This is a skeleton plugin to serve as an example and quick copy/paste for new plugins.
type BeersPlugin struct { type BeersPlugin struct {
Bot *bot.Bot Bot bot.Bot
db *sqlx.DB db *sqlx.DB
} }
@ -35,9 +35,9 @@ type untappdUser struct {
} }
// NewBeersPlugin creates a new BeersPlugin with the Plugin interface // NewBeersPlugin creates a new BeersPlugin with the Plugin interface
func NewBeersPlugin(bot *bot.Bot) *BeersPlugin { func New(bot bot.Bot) *BeersPlugin {
if bot.DBVersion == 1 { if bot.DBVersion() == 1 {
if _, err := bot.DB.Exec(`create table if not exists untappd ( if _, err := bot.DB().Exec(`create table if not exists untappd (
id integer primary key, id integer primary key,
untappdUser string, untappdUser string,
channel string, channel string,
@ -49,10 +49,10 @@ func NewBeersPlugin(bot *bot.Bot) *BeersPlugin {
} }
p := BeersPlugin{ p := BeersPlugin{
Bot: bot, Bot: bot,
db: bot.DB, db: bot.DB(),
} }
p.LoadData() p.LoadData()
for _, channel := range bot.Config.Untappd.Channels { for _, channel := range bot.Config().Untappd.Channels {
go p.untappdLoop(channel) go p.untappdLoop(channel)
} }
return &p return &p
@ -313,7 +313,7 @@ type Beers struct {
} }
func (p *BeersPlugin) pullUntappd() ([]checkin, error) { 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/" baseUrl := "https://api.untappd.com/v4/checkin/recent/"
url := baseUrl + access_token + "&limit=25" url := baseUrl + access_token + "&limit=25"
@ -343,7 +343,7 @@ func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
} }
func (p *BeersPlugin) checkUntappd(channel string) { func (p *BeersPlugin) checkUntappd(channel string) {
token := p.Bot.Config.Untappd.Token token := p.Bot.Config().Untappd.Token
if token == "" || token == "<Your Token>" { if token == "" || token == "<Your Token>" {
log.Println("No Untappd token, cannot enable plugin.") log.Println("No Untappd token, cannot enable plugin.")
return return
@ -418,7 +418,7 @@ func (p *BeersPlugin) checkUntappd(channel string) {
} }
func (p *BeersPlugin) untappdLoop(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") log.Println("Checking every ", frequency, " seconds")

View File

@ -15,7 +15,7 @@ import (
// This is a counter plugin to count arbitrary things. // This is a counter plugin to count arbitrary things.
type CounterPlugin struct { type CounterPlugin struct {
Bot *bot.Bot Bot bot.Bot
DB *sqlx.DB DB *sqlx.DB
} }
@ -102,9 +102,9 @@ func (i *Item) Delete() error {
} }
// NewCounterPlugin creates a new CounterPlugin with the Plugin interface // NewCounterPlugin creates a new CounterPlugin with the Plugin interface
func NewCounterPlugin(bot *bot.Bot) *CounterPlugin { func New(bot bot.Bot) *CounterPlugin {
if bot.DBVersion == 1 { if bot.DBVersion() == 1 {
if _, err := bot.DB.Exec(`create table if not exists counter ( if _, err := bot.DB().Exec(`create table if not exists counter (
id integer primary key, id integer primary key,
nick string, nick string,
item string, item string,
@ -115,7 +115,7 @@ func NewCounterPlugin(bot *bot.Bot) *CounterPlugin {
} }
return &CounterPlugin{ return &CounterPlugin{
Bot: bot, Bot: bot,
DB: bot.DB, DB: bot.DB(),
} }
} }
@ -271,13 +271,6 @@ func (p *CounterPlugin) Message(message bot.Message) bool {
return false 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *CounterPlugin) Help(channel string, parts []string) { func (p *CounterPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "You can set counters incrementally by using "+ p.Bot.SendMessage(channel, "You can set counters incrementally by using "+

View File

@ -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())
}

View File

@ -15,11 +15,11 @@ import (
// This is a dice plugin to serve as an example and quick copy/paste for new plugins. // This is a dice plugin to serve as an example and quick copy/paste for new plugins.
type DicePlugin struct { type DicePlugin struct {
Bot *bot.Bot Bot bot.Bot
} }
// NewDicePlugin creates a new DicePlugin with the Plugin interface // NewDicePlugin creates a new DicePlugin with the Plugin interface
func NewDicePlugin(bot *bot.Bot) *DicePlugin { func New(bot bot.Bot) *DicePlugin {
return &DicePlugin{ return &DicePlugin{
Bot: bot, Bot: bot,
} }

View File

@ -20,7 +20,7 @@ import (
// This is a downtime plugin to monitor how much our users suck // This is a downtime plugin to monitor how much our users suck
type DowntimePlugin struct { type DowntimePlugin struct {
Bot *bot.Bot Bot bot.Bot
db *sqlx.DB db *sqlx.DB
} }
@ -100,13 +100,13 @@ func (ie idleEntries) Swap(i, j int) {
} }
// NewDowntimePlugin creates a new DowntimePlugin with the Plugin interface // NewDowntimePlugin creates a new DowntimePlugin with the Plugin interface
func NewDowntimePlugin(bot *bot.Bot) *DowntimePlugin { func New(bot bot.Bot) *DowntimePlugin {
p := DowntimePlugin{ p := DowntimePlugin{
Bot: bot, 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 ( _, err := p.db.Exec(`create table if not exists downtime (
id integer primary key, id integer primary key,
nick string, nick string,
@ -160,7 +160,7 @@ func (p *DowntimePlugin) Message(message bot.Message) bool {
for _, e := range entries { for _, e := range entries {
// filter out ZNC entries and ourself // 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) p.remove(e.nick)
} else { } else {
tops = fmt.Sprintf("%s%s: %s ", tops, e.nick, time.Now().Sub(e.lastSeen)) 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 // Empty event handler because this plugin does not do anything on event recv
func (p *DowntimePlugin) Event(kind string, message bot.Message) bool { func (p *DowntimePlugin) Event(kind string, message bot.Message) bool {
log.Println(kind, "\t", message) 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 // user joined, let's nail them for it
if kind == "NICK" { if kind == "NICK" {
p.record(strings.ToLower(message.Channel)) p.record(strings.ToLower(message.Channel))

View File

@ -196,14 +196,14 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
// FactoidPlugin provides the necessary plugin-wide needs // FactoidPlugin provides the necessary plugin-wide needs
type FactoidPlugin struct { type FactoidPlugin struct {
Bot *bot.Bot Bot bot.Bot
NotFound []string NotFound []string
LastFact *factoid LastFact *factoid
db *sqlx.DB db *sqlx.DB
} }
// NewFactoidPlugin creates a new FactoidPlugin with the Plugin interface // NewFactoidPlugin creates a new FactoidPlugin with the Plugin interface
func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin { func New(botInst bot.Bot) *FactoidPlugin {
p := &FactoidPlugin{ p := &FactoidPlugin{
Bot: botInst, Bot: botInst,
NotFound: []string{ NotFound: []string{
@ -214,7 +214,7 @@ func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin {
"NOPE! NOPE! NOPE!", "NOPE! NOPE! NOPE!",
"One time, I learned how to jump rope.", "One time, I learned how to jump rope.",
}, },
db: botInst.DB, db: botInst.DB(),
} }
_, err := p.db.Exec(`create table if not exists factoid ( _, err := p.db.Exec(`create table if not exists factoid (
@ -231,13 +231,13 @@ func NewFactoidPlugin(botInst *bot.Bot) *FactoidPlugin {
log.Fatal(err) log.Fatal(err)
} }
for _, channel := range botInst.Config.Channels { for _, channel := range botInst.Config().Channels {
go p.factTimer(channel) go p.factTimer(channel)
go func(ch string) { go func(ch string) {
// Some random time to start up // Some random time to start up
time.Sleep(time.Duration(15) * time.Second) 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{ p.sayFact(bot.Message{
Channel: ch, Channel: ch,
Body: "speed test", // BUG: This is defined in the config too 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 // factTimer spits out a fact at a given interval and with given probability
func (p *FactoidPlugin) factTimer(channel string) { 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() myLastMsg := time.Now()
for { for {
time.Sleep(time.Duration(5) * time.Second) time.Sleep(time.Duration(5) * time.Second)
@ -609,7 +609,7 @@ func (p *FactoidPlugin) factTimer(channel string) {
tdelta := time.Since(lastmsg.Time) tdelta := time.Since(lastmsg.Time)
earlier := time.Since(myLastMsg) > tdelta earlier := time.Since(myLastMsg) > tdelta
chance := rand.Float64() chance := rand.Float64()
success := chance < p.Bot.Config.QuoteChance success := chance < p.Bot.Config().QuoteChance
if success && tdelta > duration && earlier { if success && tdelta > duration && earlier {
fact := p.randomFact() fact := p.randomFact()
@ -617,9 +617,11 @@ func (p *FactoidPlugin) factTimer(channel string) {
continue continue
} }
users := p.Bot.Who(channel)
// we need to fabricate a message so that bot.Filter can operate // we need to fabricate a message so that bot.Filter can operate
message := bot.Message{ message := bot.Message{
User: &p.Bot.Users[rand.Intn(len(p.Bot.Users))], User: &users[rand.Intn(len(users))],
Channel: channel, Channel: channel,
} }
p.sayFact(message, *fact) p.sayFact(message, *fact)

View File

@ -17,17 +17,17 @@ import (
// plugins. // plugins.
type RememberPlugin struct { type RememberPlugin struct {
Bot *bot.Bot Bot bot.Bot
Log map[string][]bot.Message Log map[string][]bot.Message
db *sqlx.DB db *sqlx.DB
} }
// NewRememberPlugin creates a new RememberPlugin with the Plugin interface // NewRememberPlugin creates a new RememberPlugin with the Plugin interface
func NewRememberPlugin(b *bot.Bot) *RememberPlugin { func NewRemember(b bot.Bot) *RememberPlugin {
p := RememberPlugin{ p := RememberPlugin{
Bot: b, Bot: b,
Log: make(map[string][]bot.Message), Log: make(map[string][]bot.Message),
db: b.DB, db: b.DB(),
} }
return &p return &p
} }
@ -167,8 +167,8 @@ func (p *RememberPlugin) quoteTimer(channel string) {
for { for {
// this pisses me off: You can't multiply int * time.Duration so it // this pisses me off: You can't multiply int * time.Duration so it
// has to look ugly as shit. // has to look ugly as shit.
time.Sleep(time.Duration(p.Bot.Config.QuoteTime) * time.Minute) time.Sleep(time.Duration(p.Bot.Config().QuoteTime) * time.Minute)
chance := 1.0 / p.Bot.Config.QuoteChance chance := 1.0 / p.Bot.Config().QuoteChance
if rand.Intn(int(chance)) == 0 { if rand.Intn(int(chance)) == 0 {
msg := p.randQuote() msg := p.randQuote()
p.Bot.SendMessage(channel, msg) p.Bot.SendMessage(channel, msg)

View File

@ -18,7 +18,7 @@ import (
type FirstPlugin struct { type FirstPlugin struct {
First *FirstEntry First *FirstEntry
Bot *bot.Bot Bot bot.Bot
db *sqlx.DB db *sqlx.DB
} }
@ -46,9 +46,9 @@ func (fe *FirstEntry) save(db *sqlx.DB) error {
} }
// NewFirstPlugin creates a new FirstPlugin with the Plugin interface // NewFirstPlugin creates a new FirstPlugin with the Plugin interface
func NewFirstPlugin(b *bot.Bot) *FirstPlugin { func New(b bot.Bot) *FirstPlugin {
if b.DBVersion == 1 { 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, id integer primary key,
day integer, day integer,
time integer, time integer,
@ -62,14 +62,14 @@ func NewFirstPlugin(b *bot.Bot) *FirstPlugin {
log.Println("First plugin initialized with day:", midnight(time.Now())) log.Println("First plugin initialized with day:", midnight(time.Now()))
first, err := getLastFirst(b.DB) first, err := getLastFirst(b.DB())
if err != nil { if err != nil {
log.Fatal("Could not initialize first plugin: ", err) log.Fatal("Could not initialize first plugin: ", err)
} }
return &FirstPlugin{ return &FirstPlugin{
Bot: b, Bot: b,
db: b.DB, db: b.DB(),
First: first, First: first,
} }
} }
@ -151,7 +151,7 @@ func (p *FirstPlugin) Message(message bot.Message) bool {
} }
func (p *FirstPlugin) allowed(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)) match, err := regexp.MatchString(msg, strings.ToLower(message.Body))
if err != nil { if err != nil {
log.Println("Bad regexp: ", err) log.Println("Bad regexp: ", err)
@ -161,13 +161,13 @@ func (p *FirstPlugin) allowed(message bot.Message) bool {
return false return false
} }
} }
for _, host := range p.Bot.Config.Bad.Hosts { for _, host := range p.Bot.Config().Bad.Hosts {
if host == message.Host { if host == message.Host {
log.Println("Disallowing first: ", message.User.Name, ":", message.Body) log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
return false return false
} }
} }
for _, nick := range p.Bot.Config.Bad.Nicks { for _, nick := range p.Bot.Config().Bad.Nicks {
if nick == message.User.Name { if nick == message.User.Name {
log.Println("Disallowing first: ", message.User.Name, ":", message.Body) log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
return false return false

View File

@ -15,11 +15,11 @@ import (
) )
type LeftpadPlugin struct { type LeftpadPlugin struct {
bot *bot.Bot bot bot.Bot
} }
// New creates a new LeftpadPlugin with the Plugin interface // New creates a new LeftpadPlugin with the Plugin interface
func New(bot *bot.Bot) *LeftpadPlugin { func New(bot bot.Bot) *LeftpadPlugin {
p := LeftpadPlugin{ p := LeftpadPlugin{
bot: bot, bot: bot,
} }

View File

@ -2,7 +2,6 @@
package plugins package plugins
import "fmt"
import "github.com/velour/catbase/bot" import "github.com/velour/catbase/bot"
// Plugin interface defines the methods needed to accept a plugin // Plugin interface defines the methods needed to accept a plugin
@ -14,96 +13,3 @@ type Plugin interface {
Help() Help()
RegisterWeb() 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
}

View File

@ -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
}

View File

@ -40,14 +40,14 @@ var goatse []string = []string{
} }
type TalkerPlugin struct { type TalkerPlugin struct {
Bot *bot.Bot Bot bot.Bot
enforceNicks bool enforceNicks bool
} }
func New(bot *bot.Bot) *TalkerPlugin { func New(bot bot.Bot) *TalkerPlugin {
return &TalkerPlugin{ return &TalkerPlugin{
Bot: bot, 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 // Empty event handler because this plugin does not do anything on event recv
func (p *TalkerPlugin) Event(kind string, message bot.Message) bool { func (p *TalkerPlugin) Event(kind string, message bot.Message) bool {
sayings := p.Bot.Config.WelcomeMsgs sayings := p.Bot.Config().WelcomeMsgs
if len(sayings) == 0 { if len(sayings) == 0 {
return false 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) msg := fmt.Sprintf(sayings[rand.Intn(len(sayings))], message.User.Name)
p.Bot.SendMessage(message.Channel, msg) p.Bot.SendMessage(message.Channel, msg)
return true return true

View File

@ -12,11 +12,11 @@ import (
) )
type YourPlugin struct { type YourPlugin struct {
bot *bot.Bot bot bot.Bot
} }
// NewYourPlugin creates a new YourPlugin with the Plugin interface // 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()) rand.Seed(time.Now().Unix())
return &YourPlugin{ return &YourPlugin{
bot: bot, 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. // Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *YourPlugin) Message(message bot.Message) bool { func (p *YourPlugin) Message(message bot.Message) bool {
lower := strings.ToLower(message.Body) lower := strings.ToLower(message.Body)
config := p.bot.Config.Your config := p.bot.Config().Your
if len(message.Body) > config.MaxLength { if len(message.Body) > config.MaxLength {
return false return false
} }