Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Steve McCoy 2019-08-24 14:20:57 -04:00
commit 216862bc10
84 changed files with 5577 additions and 3612 deletions

42
.gitignore vendored
View File

@ -29,3 +29,45 @@ vendor
.vscode/ .vscode/
*.code-workspace *.code-workspace
*config.lua *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

View File

@ -1,5 +1,7 @@
# CatBase # 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. 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 ## Getting Help

View File

@ -3,13 +3,15 @@
package bot package bot
import ( import (
"database/sql" "fmt"
"html/template" "math/rand"
"log"
"net/http" "net/http"
"reflect"
"strings" "strings"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/msglog" "github.com/velour/catbase/bot/msglog"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
@ -20,7 +22,7 @@ import (
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]Plugin
pluginOrdering []string pluginOrdering []string
// Users holds information about all of our friends // Users holds information about all of our friends
@ -32,30 +34,33 @@ type bot struct {
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
logIn chan msg.Message logIn chan msg.Message
logOut chan msg.Messages logOut chan msg.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 []EndPoint
// filters registered by plugins // filters registered by plugins
filters map[string]func(string) string 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 { type Variable struct {
Variable, Value string 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 { func New(config *config.Config, connector Connector) Bot {
logIn := make(chan msg.Message) logIn := make(chan msg.Message)
logOut := make(chan msg.Messages) logOut := make(chan msg.Messages)
@ -63,94 +68,69 @@ func New(config *config.Config, connector Connector) Bot {
msglog.RunNew(logIn, logOut) msglog.RunNew(logIn, logOut)
users := []user.User{ users := []user.User{
user.User{ {
Name: config.Nick, Name: config.Get("Nick", "bot"),
}, },
} }
bot := &bot{ bot := &bot{
config: config, config: config,
plugins: make(map[string]Handler), plugins: make(map[string]Plugin),
pluginOrdering: make([]string, 0), pluginOrdering: make([]string, 0),
conn: connector, conn: connector,
users: users, users: users,
me: users[0], me: users[0],
db: config.DBConn,
logIn: logIn, logIn: logIn,
logOut: logOut, logOut: logOut,
version: config.Version, httpEndPoints: make([]EndPoint, 0),
httpEndPoints: make(map[string]string),
filters: make(map[string]func(string) string), filters: make(map[string]func(string) string),
callbacks: make(CallbackMap),
} }
bot.migrateDB() bot.migrateDB()
http.HandleFunc("/", bot.serveRoot) 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.RegisterEvent(bot.Receive)
connector.RegisterEventReceived(bot.EventReceived)
connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived)
return bot 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 // Config gets the configuration that the bot is using
func (b *bot) Config() *config.Config { func (b *bot) Config() *config.Config {
return b.config return b.config
} }
func (b *bot) DBVersion() int64 {
return b.dbVersion
}
func (b *bot) DB() *sqlx.DB { func (b *bot) DB() *sqlx.DB {
return b.db return b.config.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);`) if _, err := b.DB().Exec(`create table if not exists variables (
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 (
id integer primary key, id integer primary key,
name string, name string,
value string value string
);`); err != nil { );`); 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 // 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.plugins[name] = h
b.pluginOrdering = append(b.pluginOrdering, name) b.pluginOrdering = append(b.pluginOrdering, name)
if entry := h.RegisterWeb(); entry != nil {
b.httpEndPoints[name] = *entry
}
} }
func (b *bot) Who(channel string) []user.User { func (b *bot) Who(channel string) []user.User {
@ -162,50 +142,14 @@ func (b *bot) Who(channel string) []user.User {
return users return users
} }
var rootIndex string = ` // IsCmd checks if message is a command and returns its curtailed version
<!DOCTYPE html>
<html>
<head>
<title>Factoids</title>
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.1.0/pure-min.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
{{if .EndPoints}}
<div style="padding-top: 1em;">
<table class="pure-table">
<thead>
<tr>
<th>Plugin</th>
</tr>
</thead>
<tbody>
{{range $key, $value := .EndPoints}}
<tr>
<td><a href="{{$value}}">{{$key}}</a></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</html>
`
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
func IsCmd(c *config.Config, message string) (bool, string) { func IsCmd(c *config.Config, message string) (bool, string) {
cmdcs := c.CommandChar cmdcs := c.GetArray("CommandChar", []string{"!"})
botnick := strings.ToLower(c.Nick) botnick := strings.ToLower(c.Get("Nick", "bot"))
if botnick == "" {
log.Fatal().
Msgf(`You must run catbase -set nick -val <your bot nick>`)
}
iscmd := false iscmd := false
lowerMessage := strings.ToLower(message) lowerMessage := strings.ToLower(message)
@ -237,7 +181,7 @@ func IsCmd(c *config.Config, message string) (bool, string) {
} }
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().GetArray("Admins", []string{}) {
if nick == u { if nick == u {
return true return true
} }
@ -265,7 +209,7 @@ func (b *bot) NewUser(nick string) *user.User {
} }
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().GetArray("Admins", []string{}) {
if nick == u { if nick == u {
return true return true
} }
@ -277,3 +221,31 @@ func (b *bot) checkAdmin(nick string) bool {
func (b *bot) RegisterFilter(name string, f func(string) string) { func (b *bot) RegisterFilter(name string, f func(string) string) {
b.filters[name] = f 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
}

View File

@ -6,86 +6,55 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"reflect"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
// Handles incomming PRIVMSG requests func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interface{}) bool {
func (b *bot) MsgReceived(msg msg.Message) { log.Debug().
log.Println("Received message: ", msg) Interface("msg", msg).
Msg("Received event")
// msg := b.buildMessage(client, inMsg) // msg := b.buildMessage(client, inMsg)
// do need to look up user and fix it // 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)) 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 goto RET
} }
for _, name := range b.pluginOrdering { for _, name := range b.pluginOrdering {
p := b.plugins[name] if b.runCallback(conn, b.plugins[name], kind, msg, args...) {
if p.Message(msg) { goto RET
break
} }
} }
RET: RET:
b.logIn <- msg b.logIn <- msg
return return true
} }
// Handle incoming events func (b *bot) runCallback(conn Connector, plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool {
func (b *bot) EventReceived(msg msg.Message) { t := reflect.TypeOf(plugin).String()
log.Println("Received event: ", msg) for _, cb := range b.callbacks[t][evt] {
//msg := b.buildMessage(conn, inMsg) if cb(conn, evt, message, args...) {
for _, name := range b.pluginOrdering { return true
p := b.plugins[name]
if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body
break
} }
} }
return false
} }
// Handle incoming replys // Send a message to the connection
func (b *bot) ReplyMsgReceived(msg msg.Message, identifier string) { func (b *bot) Send(conn Connector, kind Kind, args ...interface{}) (string, error) {
log.Println("Received message: ", msg) return conn.Send(kind, args...)
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)
} }
func (b *bot) GetEmojiList() map[string]string { 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. // 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 { 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 {
name = strings.Split(strings.TrimPrefix(name, "*"), ".")[0]
topics = fmt.Sprintf("%s, %s", topics, name) topics = fmt.Sprintf("%s, %s", topics, name)
} }
b.SendMessage(channel, topics) b.Send(conn, Message, channel, topics)
} else { } else {
// trigger the proper plugin's help response // trigger the proper plugin's help response
if parts[1] == "about" { if parts[1] == "about" {
b.Help(channel, parts) b.Help(conn, channel, parts)
return return
} }
if parts[1] == "variables" { if parts[1] == "variables" {
b.listVars(channel, parts) b.listVars(conn, channel, parts)
return return
} }
plugin := b.plugins[parts[1]] for name, plugin := range b.plugins {
if plugin != nil { if strings.HasPrefix(name, "*"+parts[1]) {
plugin.Help(channel, parts) if b.runCallback(conn, plugin, Help, msg.Message{Channel: channel}, channel, parts) {
} else { return
msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", parts[1]) } else {
b.SendMessage(channel, msg) 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) { func (b *bot) getVar(varName string) (string, error) {
var text string 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 { switch {
case err == sql.ErrNoRows: case err == sql.ErrNoRows:
return "", fmt.Errorf("No factoid found") return "", fmt.Errorf("No factoid found")
case err != nil: case err != nil:
log.Fatal("getVar error: ", err) log.Fatal().Err(err).Msg("getVar error")
} }
return text, nil return text, nil
} }
func (b *bot) listVars(channel string, parts []string) { func (b *bot) listVars(conn Connector, channel string, parts []string) {
var variables []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 { if err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
msg := "I know: $who, $someone, $digit, $nonzero" msg := "I know: $who, $someone, $digit, $nonzero"
if len(variables) > 0 { if len(variables) > 0 {
msg += ", " + strings.Join(variables, ", ") 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 "+ 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.Send(conn, Message, 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(conn Connector, channel, message string, action bool) {
msg := msg.Message{ msg := msg.Message{
User: &b.me, // hack User: &b.me, // hack
Channel: channel, Channel: channel,
@ -236,9 +212,8 @@ func (b *bot) selfSaid(channel, message string, action bool) {
} }
for _, name := range b.pluginOrdering { for _, name := range b.pluginOrdering {
p := b.plugins[name] if b.runCallback(conn, b.plugins[name], SelfMessage, msg) {
if p.BotMessage(msg) { return
break
} }
} }
} }

View File

@ -9,51 +9,80 @@ import (
"github.com/velour/catbase/config" "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 { type Bot interface {
// Config allows access to the bot's configuration system
Config() *config.Config Config() *config.Config
DBVersion() int64 // DB gives access to the current database
DB() *sqlx.DB DB() *sqlx.DB
// Who lists users in a particular channel
Who(string) []user.User Who(string) []user.User
AddHandler(string, Handler) // WhoAmI gives a nick for the bot
SendMessage(string, string) string WhoAmI() string
SendAction(string, string) string // AddPlugin registers a new plugin handler
ReplyToMessageIdentifier(string, string, string) (string, bool) AddPlugin(Plugin)
ReplyToMessage(string, string, msg.Message) (string, bool) // First arg should be one of bot.Message/Reply/Action/etc
React(string, string, msg.Message) bool Send(Connector, Kind, ...interface{}) (string, error)
Edit(string, string, string) bool // First arg should be one of bot.Message/Reply/Action/etc
MsgReceived(msg.Message) Receive(Connector, Kind, msg.Message, ...interface{}) bool
ReplyMsgReceived(msg.Message, string) // Register a callback
EventReceived(msg.Message) Register(Plugin, Kind, Callback)
Filter(msg.Message, string) string Filter(msg.Message, string) string
LastMessage(string) (msg.Message, error) LastMessage(string) (msg.Message, error)
CheckAdmin(string) bool CheckAdmin(string) bool
GetEmojiList() map[string]string GetEmojiList() map[string]string
RegisterFilter(string, func(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 { type Connector interface {
RegisterEventReceived(func(message msg.Message)) RegisterEvent(Callback)
RegisterMessageReceived(func(message msg.Message))
RegisterReplyMessageReceived(func(msg.Message, string)) 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 GetEmojiList() map[string]string
Serve() error Serve() error
Who(string) []string Who(string) []string
} }
// Interface used for compatibility with the Plugin interface // Plugin interface used for compatibility with the Plugin interface
type Handler interface { // Uhh it turned empty, but we're still using it to ID plugins
Message(message msg.Message) bool type Plugin interface {
Event(kind string, message msg.Message) bool
ReplyMessage(msg.Message, string) bool
BotMessage(message msg.Message) bool
Help(channel string, parts []string)
RegisterWeb() *string
} }

View File

@ -4,11 +4,12 @@ package bot
import ( import (
"fmt" "fmt"
"log" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
@ -19,85 +20,94 @@ type MockBot struct {
mock.Mock mock.Mock
db *sqlx.DB db *sqlx.DB
Cfg config.Config Cfg *config.Config
Messages []string Messages []string
Actions []string Actions []string
Reactions []string
} }
func (mb *MockBot) Config() *config.Config { return &mb.Cfg } func (mb *MockBot) Config() *config.Config { return mb.Cfg }
func (mb *MockBot) DBVersion() int64 { return 1 } func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB }
func (mb *MockBot) DB() *sqlx.DB { return mb.db } func (mb *MockBot) Who(string) []user.User { return []user.User{} }
func (mb *MockBot) Conn() Connector { return nil } func (mb *MockBot) WhoAmI() string { return "tester" }
func (mb *MockBot) Who(string) []user.User { return []user.User{} } func (mb *MockBot) DefaultConnector() Connector { return nil }
func (mb *MockBot) AddHandler(name string, f Handler) {} func (mb *MockBot) GetPassword() string { return "12345" }
func (mb *MockBot) SendMessage(ch string, msg string) string { func (mb *MockBot) Send(c Connector, kind Kind, args ...interface{}) (string, error) {
mb.Messages = append(mb.Messages, msg) switch kind {
return fmt.Sprintf("m-%d", len(mb.Actions)-1) 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 { func (mb *MockBot) AddPlugin(f Plugin) {}
mb.Actions = append(mb.Actions, msg) func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {}
return fmt.Sprintf("a-%d", len(mb.Actions)-1) 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) { func (mb *MockBot) Filter(msg msg.Message, s string) string { return s }
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) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil } func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil }
func (mb *MockBot) CheckAdmin(nick string) bool { return false } func (mb *MockBot) CheckAdmin(nick string) bool { return false }
func (mb *MockBot) React(channel, reaction string, message msg.Message) 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' isMessage := identifier[0] == 'm'
if !isMessage && identifier[0] != 'a' { if !isMessage && identifier[0] != 'a' {
log.Printf("failed to parse identifier: %s", identifier) err := fmt.Errorf("failed to parse identifier: %s", identifier)
return false log.Error().Err(err)
return "", err
} }
index, err := strconv.Atoi(strings.Split(identifier, "-")[1]) index, err := strconv.Atoi(strings.Split(identifier, "-")[1])
if err != nil { if err != nil {
log.Printf("failed to parse identifier: %s", identifier) err := fmt.Errorf("failed to parse identifier: %s", identifier)
return false log.Error().Err(err)
return "", err
} }
if isMessage { if isMessage {
if index < len(mb.Messages) { if index < len(mb.Messages) {
mb.Messages[index] = newMessage mb.Messages[index] = newMessage
} else { } else {
return false return "", fmt.Errorf("No message")
} }
} else { } else {
if index < len(mb.Actions) { if index < len(mb.Actions) {
mb.Actions[index] = newMessage mb.Actions[index] = newMessage
} else { } else {
return false return "", fmt.Errorf("No action")
} }
} }
return true return "", nil
}
func (mb *MockBot) ReplyMsgReceived(msg.Message, string) {
} }
func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) } func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) }
func (mb *MockBot) RegisterFilter(s string, f func(string) string) {} func (mb *MockBot) RegisterFilter(s string, f func(string) string) {}
func NewMockBot() *MockBot { func NewMockBot() *MockBot {
db, err := sqlx.Open("sqlite3_custom", ":memory:") cfg := config.ReadConfig("file::memory:?mode=memory&cache=shared")
if err != nil {
log.Fatal("Failed to open database:", err)
}
b := MockBot{ b := MockBot{
db: db, Cfg: cfg,
Messages: make([]string, 0), Messages: make([]string, 0),
Actions: 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 return &b
} }

View File

@ -12,9 +12,14 @@ type Log Messages
type Messages []Message type Messages []Message
type Message struct { type Message struct {
User *user.User User *user.User
Channel, Body string // With Slack, channel is the ID of a channel
Raw string Channel string
// With slack, channelName is the nice name of a channel
ChannelName string
Body string
IsIM bool
Raw interface{}
Command bool Command bool
Action bool Action bool
Time time.Time Time time.Time

73
bot/web.go Normal file
View File

@ -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 <b-nav-item> links
// The parent <nav> 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 = `
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Factoids</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>catbase</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.URL">{{ "{{ item.Name }}" }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
err: '',
nav: {{ .Nav }},
},
})
</script>
</body>
</html>
`

View File

@ -5,112 +5,119 @@ package config
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log" "os"
"regexp" "regexp"
"strconv"
"strings"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
sqlite3 "github.com/mattn/go-sqlite3" "github.com/mattn/go-sqlite3"
"github.com/yuin/gluamapper" "github.com/rs/zerolog/log"
lua "github.com/yuin/gopher-lua"
) )
// Config stores any system-wide startup information that cannot be easily configured via // Config stores any system-wide startup information that cannot be easily configured via
// the database // the database
type Config struct { type Config struct {
DBConn *sqlx.DB *sqlx.DB
DB struct { DBFile string
File string }
Name string
Server 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 return f
MainChannel string }
Plugins []string
Type string // GetInt returns the config value for a string key
Irc struct { // It will first look in the env vars for the key
Server, Pass string // 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 { return i
Token string }
// 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 var configValue string
IconURL string q := `select value from config where key=?`
FullName string err := c.DB.Get(&configValue, q, key)
Version string if err != nil {
CommandChar []string log.Debug().Msgf("WARN: Key %s is empty", key)
RatePerSec float64 return fallback
LogLength int
Admins []string
HttpAddr string
Untappd struct {
Token string
Freq int
Channels []string
} }
Twitch struct { return configValue
Freq int }
Users map[string][]string //channel -> usernames
ClientID string // GetArray returns the string slice config value for a string key
Authorization string // 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 return strings.Split(val, ";;")
WelcomeMsgs []string }
TwitterConsumerKey string
TwitterConsumerSecret string // Set changes the value for a configuration in the database
TwitterUserKey string // Note, this is always a string. Use the SetArray for an array helper
TwitterUserSecret string func (c *Config) Set(key, value string) error {
BadMsgs []string key = strings.ToLower(key)
Bad struct { q := `insert into config (key,value) values (?, ?)
Msgs []string on conflict(key) do update set value=?;`
Nicks []string tx, err := c.Begin()
Hosts []string if err != nil {
return err
} }
Your struct { _, err = tx.Exec(q, key, value, value)
MaxLength int if err != nil {
Replacements []Replacement return err
} }
LeftPad struct { err = tx.Commit()
MaxLen int if err != nil {
Who string return err
}
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
} }
return nil
}
func (c *Config) SetArray(key string, values []string) error {
vals := strings.Join(values, ";;")
return c.Set(key, vals)
} }
func init() { 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 // Readconfig loads the config data out of a JSON file located in cfile
func Readconfig(version, cfile string) *Config { func ReadConfig(dbpath string) *Config {
fmt.Printf("Using %s as config file.\n", cfile) if dbpath == "" {
L := lua.NewState() dbpath = "catbase.db"
if err := L.DoFile(cfile); err != nil {
panic(err)
} }
log.Info().Msgf("Using %s as database file.\n", dbpath)
var c Config sqlDB, err := sqlx.Open("sqlite3_custom", dbpath)
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)
if err != nil { 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 return &c
} }

23
config/config_test.go Normal file
View File

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

38
config/defaults.go Normal file
View File

@ -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.")
}

View File

@ -5,11 +5,11 @@ package irc
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
@ -42,9 +42,7 @@ type Irc struct {
config *config.Config config *config.Config
quit chan bool quit chan bool
eventReceived func(msg.Message) event bot.Callback
messageReceived func(msg.Message)
replyMessageReceived func(msg.Message, string)
} }
func New(c *config.Config) *Irc { func New(c *config.Config) *Irc {
@ -54,24 +52,28 @@ func New(c *config.Config) *Irc {
return &i return &i
} }
func (i *Irc) RegisterEventReceived(f func(msg.Message)) { func (i *Irc) RegisterEvent(f bot.Callback) {
i.eventReceived = f i.event = f
} }
func (i *Irc) RegisterMessageReceived(f func(msg.Message)) { func (i *Irc) Send(kind bot.Kind, args ...interface{}) (string, error) {
i.messageReceived = f switch kind {
} case bot.Reply:
case bot.Message:
func (i *Irc) RegisterReplyMessageReceived(f func(msg.Message, string)) { return i.sendMessage(args[0].(string), args[1].(string), args...)
i.replyMessageReceived = f case bot.Action:
return i.sendAction(args[0].(string), args[1].(string), args...)
default:
}
return "", nil
} }
func (i *Irc) JoinChannel(channel string) { 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}} 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 { for len(message) > 0 {
m := irc.Msg{ m := irc.Msg{
Cmd: "PRIVMSG", Cmd: "PRIVMSG",
@ -87,66 +89,64 @@ func (i *Irc) SendMessage(channel, message string) string {
} }
if throttle == nil { if throttle == nil {
ratePerSec := i.config.RatePerSec ratePerSec := i.config.GetInt("RatePerSec", 5)
throttle = time.Tick(time.Second / time.Duration(ratePerSec)) throttle = time.Tick(time.Second / time.Duration(ratePerSec))
} }
<-throttle <-throttle
i.Client.Out <- m 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 // 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" message = actionPrefix + " " + message + "\x01"
i.SendMessage(channel, message) return i.sendMessage(channel, message, args...)
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
} }
func (i *Irc) GetEmojiList() map[string]string { 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) return make(map[string]string)
} }
func (i *Irc) Serve() error { func (i *Irc) Serve() error {
if i.eventReceived == nil || i.messageReceived == nil { if i.event == nil {
return fmt.Errorf("Missing an event handler") return fmt.Errorf("Missing an event handler")
} }
var err error var err error
i.Client, err = irc.DialSSL( i.Client, err = irc.DialSSL(
i.config.Irc.Server, i.config.Get("Irc.Server", "localhost"),
i.config.Nick, i.config.Get("Nick", "bot"),
i.config.FullName, i.config.Get("FullName", "bot"),
i.config.Irc.Pass, i.config.Get("Irc.Pass", ""),
true, true,
) )
if err != nil { if err != nil {
return fmt.Errorf("%s", err) return fmt.Errorf("%s", err)
} }
for _, c := range i.config.Channels { for _, c := range i.config.GetArray("channels", []string{}) {
i.JoinChannel(c) i.JoinChannel(c)
} }
@ -164,7 +164,7 @@ func (i *Irc) handleConnection() {
close(i.Client.Out) close(i.Client.Out)
for err := range i.Client.Errors { for err := range i.Client.Errors {
if err != io.EOF { 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: case err, ok := <-i.Client.Errors:
if ok && err != io.EOF { if ok && err != io.EOF {
log.Println(err) log.Error().Err(err)
i.quit <- true i.quit <- true
return return
} }
@ -200,7 +200,7 @@ func (i *Irc) handleMsg(msg irc.Msg) {
switch msg.Cmd { switch msg.Cmd {
case irc.ERROR: case irc.ERROR:
log.Println(1, "Received error: "+msg.Raw) log.Info().Msgf("Received error: " + msg.Raw)
case irc.PING: case irc.PING:
i.Client.Out <- irc.Msg{Cmd: irc.PONG} i.Client.Out <- irc.Msg{Cmd: irc.PONG}
@ -209,56 +209,56 @@ func (i *Irc) handleMsg(msg irc.Msg) {
// OK, ignore // OK, ignore
case irc.ERR_NOSUCHNICK: case irc.ERR_NOSUCHNICK:
i.eventReceived(botMsg) fallthrough
case irc.ERR_NOSUCHCHANNEL: case irc.ERR_NOSUCHCHANNEL:
i.eventReceived(botMsg) fallthrough
case irc.RPL_MOTD: case irc.RPL_MOTD:
i.eventReceived(botMsg) fallthrough
case irc.RPL_NAMREPLY: case irc.RPL_NAMREPLY:
i.eventReceived(botMsg) fallthrough
case irc.RPL_TOPIC: case irc.RPL_TOPIC:
i.eventReceived(botMsg) fallthrough
case irc.KICK: case irc.KICK:
i.eventReceived(botMsg) fallthrough
case irc.TOPIC: case irc.TOPIC:
i.eventReceived(botMsg) fallthrough
case irc.MODE: case irc.MODE:
i.eventReceived(botMsg) fallthrough
case irc.JOIN: case irc.JOIN:
i.eventReceived(botMsg) fallthrough
case irc.PART: 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: case irc.QUIT:
os.Exit(1) 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: default:
cmd := irc.CmdNames[msg.Cmd] 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] channel := inMsg.Args[0]
if channel == i.config.Nick { if channel == i.config.Get("Nick", "bot") {
channel = inMsg.Args[0] channel = inMsg.Args[0]
} }

View File

@ -10,7 +10,6 @@ import (
"html" "html"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
@ -21,6 +20,7 @@ import (
"context" "context"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
@ -31,9 +31,10 @@ import (
type Slack struct { type Slack struct {
config *config.Config config *config.Config
url string url string
id string id string
ws *websocket.Conn token string
ws *websocket.Conn
lastRecieved time.Time lastRecieved time.Time
@ -43,9 +44,7 @@ type Slack struct {
emoji map[string]string emoji map[string]string
eventReceived func(msg.Message) event bot.Callback
messageReceived func(msg.Message)
replyMessageReceived func(msg.Message, string)
} }
var idCounter uint64 var idCounter uint64
@ -163,15 +162,44 @@ type rtmStart struct {
} }
func New(c *config.Config) *Slack { 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{ return &Slack{
config: c, config: c,
token: c.Get("slack.token", ""),
lastRecieved: time.Now(), lastRecieved: time.Now(),
users: make(map[string]string), users: make(map[string]string),
emoji: 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 { type Response struct {
OK bool `json:"ok"` OK bool `json:"ok"`
} }
@ -179,42 +207,34 @@ func checkReturnStatus(response *http.Response) bool {
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
response.Body.Close() response.Body.Close()
if err != nil { if err != nil {
log.Printf("Error reading Slack API body: %s", err) err := fmt.Errorf("Error reading Slack API body: %s", err)
return false return err
} }
var resp Response var resp Response
err = json.Unmarshal(body, &resp) err = json.Unmarshal(body, &resp)
if err != nil { if err != nil {
log.Printf("Error parsing message response: %s", err) err := fmt.Errorf("Error parsing message response: %s", err)
return false return err
} }
return resp.OK return nil
} }
func (s *Slack) RegisterEventReceived(f func(msg.Message)) { func (s *Slack) RegisterEvent(f bot.Callback) {
s.eventReceived = f s.event = f
} }
func (s *Slack) RegisterMessageReceived(f func(msg.Message)) { func (s *Slack) sendMessageType(channel, message string, meMessage bool) (string, error) {
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) {
postUrl := "https://slack.com/api/chat.postMessage" postUrl := "https://slack.com/api/chat.postMessage"
if meMessage { if meMessage {
postUrl = "https://slack.com/api/chat.meMessage" postUrl = "https://slack.com/api/chat.meMessage"
} }
nick := s.config.Nick nick := s.config.Get("Nick", "bot")
icon := s.config.IconURL icon := s.config.Get("IconURL", "https://placekitten.com/128/128")
resp, err := http.PostForm(postUrl, resp, err := http.PostForm(postUrl,
url.Values{"token": {s.config.Slack.Token}, url.Values{"token": {s.token},
"username": {nick}, "username": {nick},
"icon_url": {icon}, "icon_url": {icon},
"channel": {channel}, "channel": {channel},
@ -222,16 +242,16 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
}) })
if err != nil { 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) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { 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 { type MessageResponse struct {
OK bool `json:"ok"` OK bool `json:"ok"`
@ -244,7 +264,7 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
var mr MessageResponse var mr MessageResponse
err = json.Unmarshal(body, &mr) err = json.Unmarshal(body, &mr)
if err != nil { if err != nil {
log.Fatalf("Error parsing message response: %s", err) log.Fatal().Err(err).Msgf("Error parsing message response")
} }
if !mr.OK { if !mr.OK {
@ -256,24 +276,24 @@ func (s *Slack) SendMessageType(channel, message string, meMessage bool) (string
return mr.Timestamp, err return mr.Timestamp, err
} }
func (s *Slack) SendMessage(channel, message string) string { func (s *Slack) sendMessage(channel, message string) (string, error) {
log.Printf("Sending message to %s: %s", channel, message) log.Debug().Msgf("Sending message to %s: %s", channel, message)
identifier, _ := s.SendMessageType(channel, message, false) identifier, err := s.sendMessageType(channel, message, false)
return identifier return identifier, err
} }
func (s *Slack) SendAction(channel, message string) string { func (s *Slack) sendAction(channel, message string) (string, error) {
log.Printf("Sending action to %s: %s", channel, message) log.Debug().Msgf("Sending action to %s: %s", channel, message)
identifier, _ := s.SendMessageType(channel, "_"+message+"_", true) identifier, err := s.sendMessageType(channel, "_"+message+"_", true)
return identifier return identifier, err
} }
func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) { func (s *Slack) replyToMessageIdentifier(channel, message, identifier string) (string, error) {
nick := s.config.Nick nick := s.config.Get("Nick", "bot")
icon := s.config.IconURL icon := s.config.Get("IconURL", "https://placekitten.com/128/128")
resp, err := http.PostForm("https://slack.com/api/chat.postMessage", resp, err := http.PostForm("https://slack.com/api/chat.postMessage",
url.Values{"token": {s.config.Slack.Token}, url.Values{"token": {s.token},
"username": {nick}, "username": {nick},
"icon_url": {icon}, "icon_url": {icon},
"channel": {channel}, "channel": {channel},
@ -282,18 +302,18 @@ func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (s
}) })
if err != nil { if err != nil {
log.Printf("Error sending Slack reply: %s", err) err := fmt.Errorf("Error sending Slack reply: %s", err)
return "", false return "", err
} }
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { if err != nil {
log.Printf("Error reading Slack API body: %s", err) err := fmt.Errorf("Error reading Slack API body: %s", err)
return "", false return "", err
} }
log.Println(string(body)) log.Debug().Msgf("%s", body)
type MessageResponse struct { type MessageResponse struct {
OK bool `json:"ok"` OK bool `json:"ok"`
@ -303,47 +323,47 @@ func (s *Slack) ReplyToMessageIdentifier(channel, message, identifier string) (s
var mr MessageResponse var mr MessageResponse
err = json.Unmarshal(body, &mr) err = json.Unmarshal(body, &mr)
if err != nil { if err != nil {
log.Printf("Error parsing message response: %s", err) err := fmt.Errorf("Error parsing message response: %s", err)
return "", false return "", err
} }
if !mr.OK { 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) { func (s *Slack) replyToMessage(channel, message string, replyTo msg.Message) (string, error) {
return s.ReplyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"]) return s.replyToMessageIdentifier(channel, message, replyTo.AdditionalData["RAW_SLACK_TIMESTAMP"])
} }
func (s *Slack) React(channel, reaction string, message msg.Message) bool { func (s *Slack) react(channel, reaction string, message msg.Message) (string, error) {
log.Printf("Reacting in %s: %s", channel, reaction) log.Debug().Msgf("Reacting in %s: %s", channel, reaction)
resp, err := http.PostForm("https://slack.com/api/reactions.add", resp, err := http.PostForm("https://slack.com/api/reactions.add",
url.Values{"token": {s.config.Slack.Token}, url.Values{"token": {s.token},
"name": {reaction}, "name": {reaction},
"channel": {channel}, "channel": {channel},
"timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}}) "timestamp": {message.AdditionalData["RAW_SLACK_TIMESTAMP"]}})
if err != nil { if err != nil {
log.Printf("reaction failed: %s", err) err := fmt.Errorf("reaction failed: %s", err)
return false return "", err
} }
return checkReturnStatus(resp) return "", checkReturnStatus(resp)
} }
func (s *Slack) Edit(channel, newMessage, identifier string) bool { func (s *Slack) edit(channel, newMessage, identifier string) (string, error) {
log.Printf("Editing in (%s) %s: %s", identifier, channel, newMessage) log.Debug().Msgf("Editing in (%s) %s: %s", identifier, channel, newMessage)
resp, err := http.PostForm("https://slack.com/api/chat.update", resp, err := http.PostForm("https://slack.com/api/chat.update",
url.Values{"token": {s.config.Slack.Token}, url.Values{"token": {s.token},
"channel": {channel}, "channel": {channel},
"text": {newMessage}, "text": {newMessage},
"ts": {identifier}}) "ts": {identifier}})
if err != nil { if err != nil {
log.Printf("edit failed: %s", err) err := fmt.Errorf("edit failed: %s", err)
return false return "", err
} }
return checkReturnStatus(resp) return "", checkReturnStatus(resp)
} }
func (s *Slack) GetEmojiList() map[string]string { func (s *Slack) GetEmojiList() map[string]string {
@ -352,16 +372,16 @@ func (s *Slack) GetEmojiList() map[string]string {
func (s *Slack) populateEmojiList() { func (s *Slack) populateEmojiList() {
resp, err := http.PostForm("https://slack.com/api/emoji.list", 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 { 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 return
} }
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { 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 { type EmojiListResponse struct {
@ -372,7 +392,7 @@ func (s *Slack) populateEmojiList() {
var list EmojiListResponse var list EmojiListResponse
err = json.Unmarshal(body, &list) err = json.Unmarshal(body, &list)
if err != nil { if err != nil {
log.Fatalf("Error parsing emoji list: %s", err) log.Fatal().Err(err).Msgf("Error parsing emoji list")
} }
s.emoji = list.Emoji s.emoji = list.Emoji
} }
@ -397,7 +417,7 @@ func (s *Slack) receiveMessage() (slackMessage, error) {
m := slackMessage{} m := slackMessage{}
err := s.ws.Recv(context.TODO(), &m) err := s.ws.Recv(context.TODO(), &m)
if err != nil { if err != nil {
log.Println("Error decoding WS message") log.Error().Msgf("Error decoding WS message")
panic(fmt.Errorf("%v\n%v", m, err)) panic(fmt.Errorf("%v\n%v", m, err))
} }
return m, nil return m, nil
@ -422,7 +442,7 @@ func (s *Slack) Serve() error {
for { for {
msg, err := s.receiveMessage() msg, err := s.receiveMessage()
if err != nil && err == io.EOF { if err != nil && err == io.EOF {
log.Fatalf("Slack API EOF") log.Fatal().Msg("Slack API EOF")
} else if err != nil { } else if err != nil {
return fmt.Errorf("Slack API error: %s", err) return fmt.Errorf("Slack API error: %s", err)
} }
@ -432,19 +452,19 @@ func (s *Slack) Serve() error {
if !isItMe && !msg.Hidden && msg.ThreadTs == "" { if !isItMe && !msg.Hidden && msg.ThreadTs == "" {
m := s.buildMessage(msg) m := s.buildMessage(msg)
if m.Time.Before(s.lastRecieved) { 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 { } else {
s.lastRecieved = m.Time s.lastRecieved = m.Time
s.messageReceived(m) s.event(s, bot.Message, m)
} }
} else if msg.ThreadTs != "" { } else if msg.ThreadTs != "" {
//we're throwing away some information here by not parsing the correct reply object type, but that's okay //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 { } else {
log.Printf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID) log.Debug().Msgf("THAT MESSAGE WAS HIDDEN: %+v", msg.ID)
} }
case "error": 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 "": // what even is this?
case "hello": case "hello":
case "presence_change": case "presence_change":
@ -455,7 +475,7 @@ func (s *Slack) Serve() error {
// squeltch this stuff // squeltch this stuff
continue continue
default: 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 // markAllChannelsRead gets a list of all channels and marks each as read
func (s *Slack) markAllChannelsRead() { func (s *Slack) markAllChannelsRead() {
chs := s.getAllChannels() 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 { for _, ch := range chs {
s.markChannelAsRead(ch.ID) s.markChannelAsRead(ch.ID)
} }
log.Printf("Finished marking channels read") log.Debug().Msgf("Finished marking channels read")
} }
// getAllChannels returns info for all channels joined // getAllChannels returns info for all channels joined
func (s *Slack) getAllChannels() []slackChannelListItem { func (s *Slack) getAllChannels() []slackChannelListItem {
u := s.url + "channels.list" u := s.url + "channels.list"
resp, err := http.PostForm(u, resp, err := http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}}) url.Values{"token": {s.token}})
if err != nil { if err != nil {
log.Printf("Error posting user info request: %s", log.Error().Err(err).Msgf("Error posting user info request")
err)
return nil return nil
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d", log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode) resp.StatusCode)
return nil return nil
} }
@ -560,7 +579,7 @@ func (s *Slack) getAllChannels() []slackChannelListItem {
var chanInfo slackChannelListResp var chanInfo slackChannelListResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo) err = json.NewDecoder(resp.Body).Decode(&chanInfo)
if err != nil || !chanInfo.Ok { if err != nil || !chanInfo.Ok {
log.Println("Error decoding response: ", err) log.Error().Err(err).Msgf("Error decoding response")
return nil return nil
} }
return chanInfo.Channels return chanInfo.Channels
@ -570,66 +589,62 @@ func (s *Slack) getAllChannels() []slackChannelListItem {
func (s *Slack) markChannelAsRead(slackChanId string) error { func (s *Slack) markChannelAsRead(slackChanId string) error {
u := s.url + "channels.info" u := s.url + "channels.info"
resp, err := http.PostForm(u, resp, err := http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}, "channel": {slackChanId}}) url.Values{"token": {s.token}, "channel": {slackChanId}})
if err != nil { if err != nil {
log.Printf("Error posting user info request: %s", log.Error().Err(err).Msgf("Error posting user info request")
err)
return err return err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d", log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode) resp.StatusCode)
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
var chanInfo slackChannelInfoResp var chanInfo slackChannelInfoResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo) err = json.NewDecoder(resp.Body).Decode(&chanInfo)
log.Printf("%+v, %+v", err, chanInfo)
if err != nil || !chanInfo.Ok { if err != nil || !chanInfo.Ok {
log.Println("Error decoding response: ", err) log.Error().Err(err).Msgf("Error decoding response")
return err return err
} }
u = s.url + "channels.mark" u = s.url + "channels.mark"
resp, err = http.PostForm(u, 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 { if err != nil {
log.Printf("Error posting user info request: %s", log.Error().Err(err).Msgf("Error posting user info request")
err)
return err return err
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d", log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode) resp.StatusCode)
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
var markInfo map[string]interface{} var markInfo map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&markInfo) err = json.NewDecoder(resp.Body).Decode(&markInfo)
log.Printf("%+v, %+v", err, markInfo)
if err != nil { if err != nil {
log.Println("Error decoding response: ", err) log.Error().Err(err).Msgf("Error decoding response")
return err return err
} }
log.Printf("Marked %s as read", slackChanId) log.Info().Msgf("Marked %s as read", slackChanId)
return nil return nil
} }
func (s *Slack) connect() { func (s *Slack) connect() {
token := s.config.Slack.Token token := s.token
u := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token) u := fmt.Sprintf("https://slack.com/api/rtm.connect?token=%s", token)
resp, err := http.Get(u) resp, err := http.Get(u)
if err != nil { if err != nil {
return return
} }
if resp.StatusCode != 200 { 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) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { 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 var rtm rtmStart
err = json.Unmarshal(body, &rtm) err = json.Unmarshal(body, &rtm)
@ -638,7 +653,7 @@ func (s *Slack) connect() {
} }
if !rtm.Ok { if !rtm.Ok {
log.Fatalf("Slack error: %s", rtm.Error) log.Fatal().Msgf("Slack error: %s", rtm.Error)
} }
s.url = "https://slack.com/api/" s.url = "https://slack.com/api/"
@ -650,7 +665,7 @@ func (s *Slack) connect() {
rtmURL, _ := url.Parse(rtm.URL) rtmURL, _ := url.Parse(rtm.URL)
s.ws, err = websocket.Dial(context.TODO(), rtmURL) s.ws, err = websocket.Dial(context.TODO(), rtmURL)
if err != nil { 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 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" u := s.url + "users.info"
resp, err := http.PostForm(u, 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 { if err != nil || resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d %s", log.Error().Err(err).Msgf("Error posting user info request: %d",
resp.StatusCode, err) resp.StatusCode)
return "UNKNOWN", false return "UNKNOWN", false
} }
defer resp.Body.Close() defer resp.Body.Close()
var userInfo slackUserInfoResp var userInfo slackUserInfoResp
err = json.NewDecoder(resp.Body).Decode(&userInfo) err = json.NewDecoder(resp.Body).Decode(&userInfo)
if err != nil { if err != nil {
log.Println("Error decoding response: ", err) log.Error().Err(err).Msgf("Error decoding response")
return "UNKNOWN", false return "UNKNOWN", false
} }
s.users[id] = userInfo.User.Name 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 // Who gets usernames out of a channel
func (s *Slack) Who(id string) []string { 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" u := s.url + "channels.info"
resp, err := http.PostForm(u, resp, err := http.PostForm(u,
url.Values{"token": {s.config.Slack.Token}, "channel": {id}}) url.Values{"token": {s.token}, "channel": {id}})
if err != nil { if err != nil {
log.Printf("Error posting user info request: %s", log.Error().Err(err).Msgf("Error posting user info request")
err)
return []string{} return []string{}
} }
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
log.Printf("Error posting user info request: %d", log.Error().Msgf("Error posting user info request: %d",
resp.StatusCode) resp.StatusCode)
return []string{} return []string{}
} }
@ -700,17 +716,17 @@ func (s *Slack) Who(id string) []string {
var chanInfo slackChannelInfoResp var chanInfo slackChannelInfoResp
err = json.NewDecoder(resp.Body).Decode(&chanInfo) err = json.NewDecoder(resp.Body).Decode(&chanInfo)
if err != nil || !chanInfo.Ok { if err != nil || !chanInfo.Ok {
log.Println("Error decoding response: ", err) log.Error().Err(err).Msgf("Error decoding response")
return []string{} return []string{}
} }
log.Printf("%#v", chanInfo.Channel) log.Debug().Msgf("%#v", chanInfo.Channel)
handles := []string{} handles := []string{}
for _, member := range chanInfo.Channel.Members { for _, member := range chanInfo.Channel.Members {
u, _ := s.getUser(member) u, _ := s.getUser(member)
handles = append(handles, u) handles = append(handles, u)
} }
log.Printf("Returning %d handles", len(handles)) log.Debug().Msgf("Returning %d handles", len(handles))
return handles return handles
} }

View File

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

View File

@ -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 <slackapp.log.dir>/<channel>.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()
}

View File

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

View File

@ -1,123 +0,0 @@
config = {
Channels = {
"#CatBaseTest"
},
TwitterConsumerSecret = "<Consumer Secret>",
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 = "<Your 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 = "<User Key>",
MainChannel = "#CatBaseTest",
TwitterUserSecret = "<User Secret>",
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 = "<your slack token>"
},
TwitterConsumerKey = "<Consumer Key>",
Babbler = {
DefaultUsers = {
"seabass"
}
},
Type = "slack",
Admins = {
"<Admin Nick>"
},
Stats = {
Sightings = {
"user"
},
DBPath = "stats.db"
},
HttpAddr = "127.0.0.1:1337",
Inventory = {
Max = 5
},
Sisyphus = {
MinDecrement = 10,
MinPush = 1
}
}
}

45
go.mod
View File

@ -1,25 +1,46 @@
module github.com/velour/catbase module github.com/velour/catbase
require ( require (
github.com/PuerkitoBio/goquery v1.5.0 // indirect github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c
github.com/boltdb/bolt v1.3.1 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/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/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/jmoiron/sqlx v1.2.0
github.com/mattn/go-sqlite3 v1.10.0 github.com/jung-kurt/gofpdf v1.7.0 // indirect
github.com/mitchellh/mapstructure v1.1.2 // 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/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect 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/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
github.com/stretchr/objx v0.1.1 // indirect github.com/rs/zerolog v1.15.0
github.com/stretchr/testify v1.2.2 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/chat v0.0.0-20180713122344-fd1d1606cb89
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec golang.org/x/mobile v0.0.0-20190806162312-597adff16ade // indirect
golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
golang.org/x/text v0.3.0 // 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 gopkg.in/sourcemap.v1 v1.0.5 // indirect
) )

146
go.sum
View File

@ -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 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 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 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 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 h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.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 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 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 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 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.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.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 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E=
github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU= 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 h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY= 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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.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 h1:3D3M900hEBJJAqyKl70QuRHi5weX9+ptlQI1v+FNcQ8=
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM= 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 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE= 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/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec h1:vpF8Kxql6/3OvGH4y2SKtpN3WsB17mvJ8f8H1o2vucQ= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac= 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-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-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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= 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=

120
main.go
View File

@ -4,19 +4,26 @@ package main
import ( import (
"flag" "flag"
"log" "github.com/velour/catbase/plugins/cli"
"github.com/velour/catbase/plugins/newsbid"
"math/rand" "math/rand"
"net/http"
"os"
"time" "time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"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/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/admin"
"github.com/velour/catbase/plugins/babbler" "github.com/velour/catbase/plugins/babbler"
"github.com/velour/catbase/plugins/beers" "github.com/velour/catbase/plugins/beers"
"github.com/velour/catbase/plugins/couldashouldawoulda" "github.com/velour/catbase/plugins/couldashouldawoulda"
"github.com/velour/catbase/plugins/counter" "github.com/velour/catbase/plugins/counter"
"github.com/velour/catbase/plugins/db"
"github.com/velour/catbase/plugins/dice" "github.com/velour/catbase/plugins/dice"
"github.com/velour/catbase/plugins/emojifyme" "github.com/velour/catbase/plugins/emojifyme"
"github.com/velour/catbase/plugins/fact" "github.com/velour/catbase/plugins/fact"
@ -26,68 +33,107 @@ import (
"github.com/velour/catbase/plugins/nerdepedia" "github.com/velour/catbase/plugins/nerdepedia"
"github.com/velour/catbase/plugins/picker" "github.com/velour/catbase/plugins/picker"
"github.com/velour/catbase/plugins/reaction" "github.com/velour/catbase/plugins/reaction"
"github.com/velour/catbase/plugins/remember"
"github.com/velour/catbase/plugins/reminder" "github.com/velour/catbase/plugins/reminder"
"github.com/velour/catbase/plugins/rpgORdie" "github.com/velour/catbase/plugins/rpgORdie"
"github.com/velour/catbase/plugins/rss" "github.com/velour/catbase/plugins/rss"
"github.com/velour/catbase/plugins/sisyphus" "github.com/velour/catbase/plugins/sisyphus"
"github.com/velour/catbase/plugins/stock"
"github.com/velour/catbase/plugins/talker" "github.com/velour/catbase/plugins/talker"
"github.com/velour/catbase/plugins/tell" "github.com/velour/catbase/plugins/tell"
"github.com/velour/catbase/plugins/tldr"
"github.com/velour/catbase/plugins/twitch" "github.com/velour/catbase/plugins/twitch"
"github.com/velour/catbase/plugins/your" "github.com/velour/catbase/plugins/your"
"github.com/velour/catbase/plugins/zork" "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() { func main() {
rand.Seed(time.Now().Unix()) rand.Seed(time.Now().Unix())
var cfile = flag.String("config", "config.lua", var dbpath = flag.String("db", "catbase.db",
"Config file to load. (Defaults to config.lua)") "Database file to load. (Defaults to catbase.db)")
flag.Parse() // parses the logging flags. 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 <channel> <nick>"`)
} else if *initDB {
c.SetDefaults(flag.Arg(0), flag.Arg(1))
return
}
var client bot.Connector var client bot.Connector
switch c.Type { switch c.Get("type", "slackapp") {
case "irc": case "irc":
client = irc.New(c) client = irc.New(c)
case "slack": case "slack":
client = slack.New(c) client = slack.New(c)
case "slackapp":
client = slackapp.New(c)
default: 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 := bot.New(c, client)
b.AddHandler("admin", admin.New(b)) b.AddPlugin(admin.New(b))
b.AddHandler("first", first.New(b)) b.AddPlugin(emojifyme.New(b))
b.AddHandler("leftpad", leftpad.New(b)) b.AddPlugin(first.New(b))
b.AddHandler("talker", talker.New(b)) b.AddPlugin(leftpad.New(b))
b.AddHandler("dice", dice.New(b)) b.AddPlugin(talker.New(b))
b.AddHandler("picker", picker.New(b)) b.AddPlugin(dice.New(b))
b.AddHandler("beers", beers.New(b)) b.AddPlugin(picker.New(b))
b.AddHandler("remember", fact.NewRemember(b)) b.AddPlugin(beers.New(b))
b.AddHandler("your", your.New(b)) b.AddPlugin(remember.New(b))
b.AddHandler("counter", counter.New(b)) b.AddPlugin(your.New(b))
b.AddHandler("reminder", reminder.New(b)) b.AddPlugin(counter.New(b))
b.AddHandler("babbler", babbler.New(b)) b.AddPlugin(reminder.New(b))
b.AddHandler("zork", zork.New(b)) b.AddPlugin(babbler.New(b))
b.AddHandler("rss", rss.New(b)) b.AddPlugin(zork.New(b))
b.AddHandler("reaction", reaction.New(b)) b.AddPlugin(rss.New(b))
b.AddHandler("emojifyme", emojifyme.New(b)) b.AddPlugin(reaction.New(b))
b.AddHandler("twitch", twitch.New(b)) b.AddPlugin(twitch.New(b))
b.AddHandler("inventory", inventory.New(b)) b.AddPlugin(inventory.New(b))
b.AddHandler("rpgORdie", rpgORdie.New(b)) b.AddPlugin(rpgORdie.New(b))
b.AddHandler("sisyphus", sisyphus.New(b)) b.AddPlugin(sisyphus.New(b))
b.AddHandler("tell", tell.New(b)) b.AddPlugin(tell.New(b))
b.AddHandler("couldashouldawoulda", couldashouldawoulda.New(b)) b.AddPlugin(couldashouldawoulda.New(b))
b.AddHandler("nedepedia", nerdepedia.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 // catches anything left, will always return true
b.AddHandler("factoid", fact.New(b)) b.AddPlugin(fact.New(b))
b.AddHandler("db", db.New(b))
for { if err := client.Serve(); err != nil {
err := client.Serve() log.Fatal().Err(err)
log.Println(err)
} }
addr := c.Get("HttpAddr", "127.0.0.1:1337")
log.Fatal().Err(http.ListenAndServe(addr, nil))
} }

View File

@ -3,55 +3,121 @@
package admin package admin
import ( import (
"log" "encoding/json"
"fmt"
"html/template"
"net/http"
"strings" "strings"
"time"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "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. // 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
cfg *config.Config
quiet bool
} }
// NewAdminPlugin creates a new AdminPlugin with the Plugin interface // NewAdminPlugin creates a new AdminPlugin with the Plugin interface
func New(bot bot.Bot) *AdminPlugin { func New(b bot.Bot) *AdminPlugin {
p := &AdminPlugin{ p := &AdminPlugin{
Bot: bot, bot: b,
db: bot.DB(), 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 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. // 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. // 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. // 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 body := message.Body
if p.quiet {
return true
}
if len(body) > 0 && body[0] == '$' { 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], "<unknown>")
p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("%s: %s", parts[1], v))
return true
} }
return false 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 { if parts := strings.SplitN(message.Body, "!=", 2); len(parts) == 2 {
variable := strings.ToLower(strings.TrimSpace(parts[0])) variable := strings.ToLower(strings.TrimSpace(parts[0]))
value := strings.TrimSpace(parts[1]) value := strings.TrimSpace(parts[1])
_, err := p.db.Exec(`delete from variables where name=? and value=?`, variable, value) _, err := p.db.Exec(`delete from variables where name=? and value=?`, variable, value)
if err != nil { if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.") p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err) log.Error().Err(err)
} else { } else {
p.Bot.SendMessage(message.Channel, "Removed.") p.bot.Send(conn, bot.Message, message.Channel, "Removed.")
} }
return true 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) row := p.db.QueryRow(`select count(*) from variables where value = ?`, variable, value)
err := row.Scan(&count) err := row.Scan(&count)
if err != nil { if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.") p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err) log.Error().Err(err)
return true return true
} }
if count > 0 { 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 { } else {
_, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value) _, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value)
if err != nil { if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.") p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err) log.Error().Err(err)
return true return true
} }
p.Bot.SendMessage(message.Channel, "Added.") p.bot.Send(conn, bot.Message, message.Channel, "Added.")
} }
return true 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *AdminPlugin) Help(channel string, parts []string) { func (p *AdminPlugin) help(conn bot.Connector, kind bot.Kind, m msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "This does super secret things that you're not allowed to know about.") 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) registerWeb() {
func (p *AdminPlugin) Event(kind string, message msg.Message) bool { http.HandleFunc("/vars/api", p.handleWebAPI)
return false http.HandleFunc("/vars", p.handleWeb)
p.bot.RegisterWeb("/vars", "Variables")
} }
// Handler for bot's own messages var tpl = template.Must(template.New("factoidIndex").Parse(varIndex))
func (p *AdminPlugin) BotMessage(message msg.Message) bool {
return false 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) handleWebAPI(w http.ResponseWriter, r *http.Request) {
func (p *AdminPlugin) RegisterWeb() *string { var configEntries []struct {
return nil 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 }

View File

@ -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: <unknown>"
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)
}

75
plugins/admin/index.go Normal file
View File

@ -0,0 +1,75 @@
package admin
var varIndex = `
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Vars</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>Variables</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.URL" :active="item.Name === 'Variables'">{{ "{{ item.Name }}" }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ "{{ err }}" }}
</b-alert>
<b-container>
<b-table
fixed
:items="vars"
:sort-by.sync="sortBy"
:fields="fields"></b-table>
</b-container>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
err: '',
nav: {{ .Nav }},
vars: [],
sortBy: 'key',
fields: [
{ key: { sortable: true } },
'value'
]
},
mounted() {
this.getData();
},
methods: {
getData: function() {
axios.get('/vars/api')
.then(resp => {
this.vars = resp.data;
})
.catch(err => this.err = err);
}
}
})
</script>
</body>
</html>
`

View File

@ -6,14 +6,14 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"strings" "strings"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
) )
var ( var (
@ -25,7 +25,6 @@ var (
type BabblerPlugin struct { type BabblerPlugin struct {
Bot bot.Bot Bot bot.Bot
db *sqlx.DB db *sqlx.DB
config *config.Config
WithGoRoutines bool WithGoRoutines bool
} }
@ -54,55 +53,55 @@ type BabblerArc struct {
Frequency int64 `db:"frequency"` Frequency int64 `db:"frequency"`
} }
func New(bot bot.Bot) *BabblerPlugin { func New(b bot.Bot) *BabblerPlugin {
log.SetFlags(log.LstdFlags | log.Lshortfile) if _, err := b.DB().Exec(`create table if not exists babblers (
if _, err := bot.DB().Exec(`create table if not exists babblers (
id integer primary key, id integer primary key,
babbler string babbler string
);`); err != nil { );`); 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, id integer primary key,
word string word string
);`); err != nil { );`); 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, id integer primary key,
babblerId integer, babblerId integer,
wordId integer, wordId integer,
root integer, root integer,
rootFrequency integer rootFrequency integer
);`); err != nil { );`); 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, id integer primary key,
fromNodeId integer, fromNodeId integer,
toNodeId interger, toNodeId interger,
frequency integer frequency integer
);`); err != nil { );`); err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
plugin := &BabblerPlugin{ plugin := &BabblerPlugin{
Bot: bot, Bot: b,
db: bot.DB(), db: b.DB(),
config: bot.Config(),
WithGoRoutines: true, WithGoRoutines: true,
} }
plugin.createNewWord("") plugin.createNewWord("")
b.Register(plugin, bot.Message, plugin.message)
b.Register(plugin, bot.Help, plugin.help)
return plugin 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) lowercase := strings.ToLower(message.Body)
tokens := strings.Fields(lowercase) tokens := strings.Fields(lowercase)
numTokens := len(tokens) numTokens := len(tokens)
@ -144,12 +143,12 @@ func (p *BabblerPlugin) Message(message msg.Message) bool {
} }
if saidSomething { if saidSomething {
p.Bot.SendMessage(message.Channel, saidWhat) p.Bot.Send(c, bot.Message, message.Channel, saidWhat)
} }
return saidSomething 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{ commands := []string{
"initialize babbler for seabass", "initialize babbler for seabass",
"merge babbler drseabass into seabass", "merge babbler drseabass into seabass",
@ -158,19 +157,8 @@ func (p *BabblerPlugin) Help(channel string, parts []string) {
"seabass says-middle-out ...", "seabass says-middle-out ...",
"seabass says-bridge ... | ...", "seabass says-bridge ... | ...",
} }
p.Bot.SendMessage(channel, strings.Join(commands, "\n\n")) p.Bot.Send(c, bot.Message, msg.Channel, strings.Join(commands, "\n\n"))
} return true
func (p *BabblerPlugin) Event(kind string, message msg.Message) bool {
return false
}
func (p *BabblerPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *BabblerPlugin) RegisterWeb() *string {
return nil
} }
func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) { func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) {
@ -178,7 +166,7 @@ func (p *BabblerPlugin) makeBabbler(name string) (*Babbler, error) {
if err == nil { if err == nil {
id, err := res.LastInsertId() id, err := res.LastInsertId()
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
return &Babbler{ 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) err := p.db.QueryRowx(`select * from babblers where babbler = ? LIMIT 1;`, name).StructScan(&bblr)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
log.Printf("failed to find babbler") log.Error().Msg("failed to find babbler")
return nil, NO_BABBLER return nil, NO_BABBLER
} }
log.Printf("encountered problem in babbler lookup") log.Error().Err(err).Msg("encountered problem in babbler lookup")
log.Print(err)
return nil, err return nil, err
} }
return &bblr, nil return &bblr, nil
@ -209,13 +196,13 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
if err == NO_BABBLER { if err == NO_BABBLER {
babbler, err = p.makeBabbler(name) babbler, err = p.makeBabbler(name)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
rows, err := p.db.Queryx(fmt.Sprintf("select tidbit from factoid where fact like '%s quotes';", babbler.Name)) rows, err := p.db.Queryx(fmt.Sprintf("select tidbit from factoid where fact like '%s quotes';", babbler.Name))
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return babbler, nil return babbler, nil
} }
defer rows.Close() defer rows.Close()
@ -225,10 +212,10 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
var tidbit string var tidbit string
err := rows.Scan(&tidbit) err := rows.Scan(&tidbit)
log.Print(tidbit) log.Debug().Str("tidbit", tidbit)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return babbler, err return babbler, err
} }
tidbits = append(tidbits, tidbit) tidbits = append(tidbits, tidbit)
@ -236,7 +223,7 @@ func (p *BabblerPlugin) getOrCreateBabbler(name string) (*Babbler, error) {
for _, tidbit := range tidbits { for _, tidbit := range tidbits {
if err = p.addToMarkovChain(babbler, tidbit); err != nil { 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) { func (p *BabblerPlugin) createNewWord(word string) (*BabblerWord, error) {
res, err := p.db.Exec(`insert into babblerWords (word) values (?);`, word) res, err := p.db.Exec(`insert into babblerWords (word) values (?);`, word)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
return &BabblerWord{ return &BabblerWord{
@ -277,7 +264,7 @@ func (p *BabblerPlugin) getOrCreateWord(word string) (*BabblerWord, error) {
return p.createNewWord(word) return p.createNewWord(word)
} else { } else {
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
} }
return w, 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) { func (p *BabblerPlugin) createBabblerNode(babbler *Babbler, word string) (*BabblerNode, error) {
w, err := p.getOrCreateWord(word) w, err := p.getOrCreateWord(word)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
res, err := p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?, ?, 0, 0)`, babbler.BabblerId, w.WordId) res, err := p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?, ?, 0, 0)`, babbler.BabblerId, w.WordId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, 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) { func (p *BabblerPlugin) incrementRootWordFrequency(babbler *Babbler, word string) (*BabblerNode, error) {
node, err := p.getOrCreateBabblerNode(babbler, word) node, err := p.getOrCreateBabblerNode(babbler, word)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
_, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + 1, root = 1 where id = ?;`, node.NodeId) _, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + 1, root = 1 where id = ?;`, node.NodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
node.RootFrequency += 1 node.RootFrequency += 1
@ -365,7 +352,7 @@ func (p *BabblerPlugin) getBabblerArc(fromNode, toNode *BabblerNode) (*BabblerAr
func (p *BabblerPlugin) incrementWordArc(fromNode, toNode *BabblerNode) (*BabblerArc, error) { 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) res, err := p.db.Exec(`update babblerArcs set frequency = frequency + 1 where fromNodeId = ? and toNodeId = ?;`, fromNode.NodeId, toNode.NodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
@ -377,7 +364,7 @@ func (p *BabblerPlugin) incrementWordArc(fromNode, toNode *BabblerNode) (*Babble
if affectedRows == 0 { if affectedRows == 0 {
res, err = p.db.Exec(`insert into babblerArcs (fromNodeId, toNodeId, frequency) values (?, ?, 1);`, fromNode.NodeId, toNode.NodeId) res, err = p.db.Exec(`insert into babblerArcs (fromNodeId, toNodeId, frequency) values (?, ?, 1);`, fromNode.NodeId, toNode.NodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
} }
@ -401,19 +388,19 @@ func (p *BabblerPlugin) addToMarkovChain(babbler *Babbler, phrase string) error
curNode, err := p.incrementRootWordFrequency(babbler, words[0]) curNode, err := p.incrementRootWordFrequency(babbler, words[0])
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
for i := 1; i < len(words); i++ { for i := 1; i < len(words); i++ {
nextNode, err := p.getOrCreateBabblerNode(babbler, words[i]) nextNode, err := p.getOrCreateBabblerNode(babbler, words[i])
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
_, err = p.incrementWordArc(curNode, nextNode) _, err = p.incrementWordArc(curNode, nextNode)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
curNode = nextNode curNode = nextNode
@ -426,7 +413,7 @@ func (p *BabblerPlugin) addToMarkovChain(babbler *Babbler, phrase string) error
func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *BabblerWord, 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) rows, err := p.db.Queryx(`select * from babblerNodes where babblerId = ? and root = 1;`, babbler.BabblerId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
defer rows.Close() defer rows.Close()
@ -438,7 +425,7 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba
var node BabblerNode var node BabblerNode
err = rows.StructScan(&node) err = rows.StructScan(&node)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
rootNodes = append(rootNodes, &node) rootNodes = append(rootNodes, &node)
@ -457,21 +444,21 @@ func (p *BabblerPlugin) getWeightedRootNode(babbler *Babbler) (*BabblerNode, *Ba
var w BabblerWord var w BabblerWord
err := p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) err := p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
return node, &w, nil return node, &w, nil
} }
} }
log.Fatalf("shouldn't happen") log.Fatal().Msg("failed to find weighted root word")
return nil, nil, errors.New("failed to find weighted root word") return nil, nil, nil
} }
func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode, *BabblerWord, error) { func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode, *BabblerWord, error) {
rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, fromNode.NodeId) rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, fromNode.NodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
defer rows.Close() defer rows.Close()
@ -482,7 +469,7 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode
var arc BabblerArc var arc BabblerArc
err = rows.StructScan(&arc) err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
arcs = append(arcs, &arc) arcs = append(arcs, &arc)
@ -503,28 +490,28 @@ func (p *BabblerPlugin) getWeightedNextWord(fromNode *BabblerNode) (*BabblerNode
var node BabblerNode var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.ToNodeId).StructScan(&node) err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.ToNodeId).StructScan(&node)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
var w BabblerWord var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
return &node, &w, nil return &node, &w, nil
} }
} }
log.Fatalf("shouldn't happen") log.Fatal().Msg("failed to find weighted next word")
return nil, nil, errors.New("failed to find weighted next word") return nil, nil, nil
} }
func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNode, *BabblerWord, bool, error) { func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNode, *BabblerWord, bool, error) {
rows, err := p.db.Queryx(`select * from babblerArcs where toNodeId = ?;`, toNode.NodeId) rows, err := p.db.Queryx(`select * from babblerArcs where toNodeId = ?;`, toNode.NodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
defer rows.Close() defer rows.Close()
@ -535,7 +522,7 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo
var arc BabblerArc var arc BabblerArc
err = rows.StructScan(&arc) err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
arcs = append(arcs, &arc) arcs = append(arcs, &arc)
@ -562,39 +549,39 @@ func (p *BabblerPlugin) getWeightedPreviousWord(toNode *BabblerNode) (*BabblerNo
var node BabblerNode var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.FromNodeId).StructScan(&node) err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, arc.FromNodeId).StructScan(&node)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
var w BabblerWord var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w) err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, node.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, false, err return nil, nil, false, err
} }
return &node, &w, false, nil return &node, &w, false, nil
} }
} }
log.Fatalf("shouldn't happen") log.Fatal().Msg("failed to find weighted previous word")
return nil, nil, false, errors.New("failed to find weighted previous word") return nil, nil, false, nil
} }
func (p *BabblerPlugin) verifyPhrase(babbler *Babbler, phrase []string) (*BabblerNode, *BabblerNode, error) { func (p *BabblerPlugin) verifyPhrase(babbler *Babbler, phrase []string) (*BabblerNode, *BabblerNode, error) {
curNode, err := p.getBabblerNode(babbler, phrase[0]) curNode, err := p.getBabblerNode(babbler, phrase[0])
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
firstNode := curNode firstNode := curNode
for i := 1; i < len(phrase); i++ { for i := 1; i < len(phrase); i++ {
nextNode, err := p.getBabblerNode(babbler, phrase[i]) nextNode, err := p.getBabblerNode(babbler, phrase[i])
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
_, err = p.getBabblerArc(curNode, nextNode) _, err = p.getBabblerArc(curNode, nextNode)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, nil, err return nil, nil, err
} }
curNode = nextNode curNode = nextNode
@ -610,7 +597,7 @@ func (p *BabblerPlugin) babble(who string) (string, error) {
func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, error) { func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, error) {
babbler, err := p.getBabbler(babblerName) babbler, err := p.getBabbler(babblerName)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", nil return "", nil
} }
@ -621,14 +608,14 @@ func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, e
if len(seed) == 0 { if len(seed) == 0 {
curNode, curWord, err = p.getWeightedRootNode(babbler) curNode, curWord, err = p.getWeightedRootNode(babbler)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
words = append(words, curWord.Word) words = append(words, curWord.Word)
} else { } else {
_, curNode, err = p.verifyPhrase(babbler, seed) _, curNode, err = p.verifyPhrase(babbler, seed)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
} }
@ -636,7 +623,7 @@ func (p *BabblerPlugin) babbleSeed(babblerName string, seed []string) (string, e
for { for {
curNode, curWord, err = p.getWeightedNextWord(curNode) curNode, curWord, err = p.getWeightedNextWord(curNode)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
if curWord.Word == " " { 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 { func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoName, otherName string) error {
intoNode, err := p.getOrCreateBabblerNode(intoBabbler, "<"+intoName+">") intoNode, err := p.getOrCreateBabblerNode(intoBabbler, "<"+intoName+">")
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
otherNode, err := p.getOrCreateBabblerNode(otherBabbler, "<"+otherName+">") otherNode, err := p.getOrCreateBabblerNode(otherBabbler, "<"+otherName+">")
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return 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) rows, err := p.db.Queryx("select * from babblerNodes where babblerId = ?;", otherBabbler.BabblerId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
defer rows.Close() defer rows.Close()
@ -679,7 +666,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var node BabblerNode var node BabblerNode
err = rows.StructScan(&node) err = rows.StructScan(&node)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
nodes = append(nodes, &node) nodes = append(nodes, &node)
@ -695,12 +682,12 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
if node.Root > 0 { 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) 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 { if err != nil {
log.Print(err) log.Error().Err(err)
} }
} else { } else {
res, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + ? where babblerId = ? and wordId = ?;`, node.RootFrequency, intoBabbler.BabblerId, node.WordId) res, err = p.db.Exec(`update babblerNodes set rootFrequency = rootFrequency + ? where babblerId = ? and wordId = ?;`, node.RootFrequency, intoBabbler.BabblerId, node.WordId)
if err != nil { 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 { 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) res, err = p.db.Exec(`insert into babblerNodes (babblerId, wordId, root, rootFrequency) values (?,?,?,?) ;`, intoBabbler.BabblerId, node.WordId, node.Root, node.RootFrequency)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
} }
@ -720,7 +707,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var updatedNode BabblerNode var updatedNode BabblerNode
err = p.db.QueryRowx(`select * from babblerNodes where babblerId = ? and wordId = ? LIMIT 1;`, intoBabbler.BabblerId, node.WordId).StructScan(&updatedNode) err = p.db.QueryRowx(`select * from babblerNodes where babblerId = ? and wordId = ? LIMIT 1;`, intoBabbler.BabblerId, node.WordId).StructScan(&updatedNode)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
@ -740,7 +727,7 @@ func (p *BabblerPlugin) mergeBabblers(intoBabbler, otherBabbler *Babbler, intoNa
var arc BabblerArc var arc BabblerArc
err = rows.StructScan(&arc) err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return err return err
} }
arcs = append(arcs, &arc) 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) { func (p *BabblerPlugin) babbleSeedSuffix(babblerName string, seed []string) (string, error) {
babbler, err := p.getBabbler(babblerName) babbler, err := p.getBabbler(babblerName)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", nil return "", nil
} }
firstNode, curNode, err := p.verifyPhrase(babbler, seed) firstNode, curNode, err := p.verifyPhrase(babbler, seed)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
@ -777,7 +764,7 @@ func (p *BabblerPlugin) babbleSeedSuffix(babblerName string, seed []string) (str
for { for {
curNode, curWord, shouldTerminate, err = p.getWeightedPreviousWord(curNode) curNode, curWord, shouldTerminate, err = p.getWeightedPreviousWord(curNode)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
@ -806,7 +793,7 @@ func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error)
arcs := []*BabblerArc{} arcs := []*BabblerArc{}
rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, babblerNodeId) rows, err := p.db.Queryx(`select * from babblerArcs where fromNodeId = ?;`, babblerNodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return arcs, err return arcs, err
} }
defer rows.Close() defer rows.Close()
@ -815,7 +802,7 @@ func (p *BabblerPlugin) getNextArcs(babblerNodeId int64) ([]*BabblerArc, error)
var arc BabblerArc var arc BabblerArc
err = rows.StructScan(&arc) err = rows.StructScan(&arc)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return []*BabblerArc{}, err return []*BabblerArc{}, err
} }
arcs = append(arcs, &arc) arcs = append(arcs, &arc)
@ -827,7 +814,7 @@ func (p *BabblerPlugin) getBabblerNodeById(nodeId int64) (*BabblerNode, error) {
var node BabblerNode var node BabblerNode
err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, nodeId).StructScan(&node) err := p.db.QueryRowx(`select * from babblerNodes where id = ? LIMIT 1;`, nodeId).StructScan(&node)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil, err return nil, err
} }
return &node, nil return &node, nil
@ -843,19 +830,19 @@ func shuffle(a []*BabblerArc) {
func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []string) (string, error) { func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []string) (string, error) {
babbler, err := p.getBabbler(babblerName) babbler, err := p.getBabbler(babblerName)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", nil return "", nil
} }
_, startWordNode, err := p.verifyPhrase(babbler, start) _, startWordNode, err := p.verifyPhrase(babbler, start)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
endWordNode, _, err := p.verifyPhrase(babbler, end) endWordNode, _, err := p.verifyPhrase(babbler, end)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
@ -864,7 +851,7 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
previous *searchNode previous *searchNode
} }
open := []*searchNode{&searchNode{startWordNode.NodeId, nil}} open := []*searchNode{{startWordNode.NodeId, nil}}
closed := map[int64]*searchNode{startWordNode.NodeId: open[0]} closed := map[int64]*searchNode{startWordNode.NodeId: open[0]}
goalNodeId := int64(-1) goalNodeId := int64(-1)
@ -909,13 +896,13 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
for { for {
cur, err := p.getBabblerNodeById(curSearchNode.babblerNodeId) cur, err := p.getBabblerNodeById(curSearchNode.babblerNodeId)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
var w BabblerWord var w BabblerWord
err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, cur.WordId).StructScan(&w) err = p.db.QueryRowx(`select * from babblerWords where id = ? LIMIT 1;`, cur.WordId).StructScan(&w)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", err return "", err
} }
words = append(words, w.Word) words = append(words, w.Word)
@ -937,5 +924,3 @@ func (p *BabblerPlugin) babbleSeedBookends(babblerName string, start, end []stri
return strings.Join(words, " "), nil return strings.Join(words, " "), nil
} }
func (p *BabblerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -3,6 +3,7 @@
package babbler package babbler
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,13 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return c, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -28,15 +30,20 @@ func makeMessage(payload string) msg.Message {
func newBabblerPlugin(mb *bot.MockBot) *BabblerPlugin { func newBabblerPlugin(mb *bot.MockBot) *BabblerPlugin {
bp := New(mb) bp := New(mb)
bp.WithGoRoutines = false bp.WithGoRoutines = false
mb.DB().MustExec(`
delete from babblers;
delete from babblerWords;
delete from babblerNodes;
delete from babblerArcs;
`)
return bp return bp
} }
func TestBabblerNoBabbler(t *testing.T) { func TestBabblerNoBabbler(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
bp.Message(makeMessage("!seabass2 says")) bp.message(makeMessage("!seabass2 says"))
res := assert.Len(t, mb.Messages, 0) res := assert.Len(t, mb.Messages, 0)
assert.True(t, res) assert.True(t, res)
// assert.Contains(t, mb.Messages[0], "seabass2 babbler not found") // assert.Contains(t, mb.Messages[0], "seabass2 babbler not found")
@ -45,11 +52,10 @@ func TestBabblerNoBabbler(t *testing.T) {
func TestBabblerNothingSaid(t *testing.T) { func TestBabblerNothingSaid(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
res := bp.Message(makeMessage("initialize babbler for seabass")) res := bp.message(makeMessage("initialize babbler for seabass"))
assert.True(t, res) assert.True(t, res)
res = bp.Message(makeMessage("!seabass says")) res = bp.message(makeMessage("!seabass says"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
assert.Contains(t, mb.Messages[0], "okay.") assert.Contains(t, mb.Messages[0], "okay.")
@ -59,16 +65,15 @@ func TestBabblerNothingSaid(t *testing.T) {
func TestBabbler(t *testing.T) { func TestBabbler(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says")) res = bp.message(makeMessage("!seabass says"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is") assert.Contains(t, mb.Messages[0], "this is")
@ -78,16 +83,15 @@ func TestBabbler(t *testing.T) {
func TestBabblerSeed(t *testing.T) { func TestBabblerSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says long")) res = bp.message(makeMessage("!seabass says long"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "long message") assert.Contains(t, mb.Messages[0], "long message")
@ -96,16 +100,15 @@ func TestBabblerSeed(t *testing.T) {
func TestBabblerMultiSeed(t *testing.T) { func TestBabblerMultiSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says This is a long")) res = bp.message(makeMessage("!seabass says This is a long"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is a long message") 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) { func TestBabblerMultiSeed2(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says is a long")) res = bp.message(makeMessage("!seabass says is a long"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "is a long message") assert.Contains(t, mb.Messages[0], "is a long message")
@ -132,16 +134,15 @@ func TestBabblerMultiSeed2(t *testing.T) {
func TestBabblerBadSeed(t *testing.T) { func TestBabblerBadSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
bp.Message(seabass) bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
bp.Message(seabass) bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
bp.Message(seabass) bp.message(c, k, seabass)
bp.Message(makeMessage("!seabass says noooo this is bad")) bp.message(makeMessage("!seabass says noooo this is bad"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "seabass never said 'noooo this is bad'") 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) { func TestBabblerBadSeed2(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is a message") c, k, seabass := makeMessage("This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
bp.Message(seabass) bp.message(c, k, seabass)
seabass.Body = "This is another message" seabass.Body = "This is another message"
bp.Message(seabass) bp.message(c, k, seabass)
seabass.Body = "This is a long message" seabass.Body = "This is a long message"
bp.Message(seabass) bp.message(c, k, seabass)
bp.Message(makeMessage("!seabass says This is a really")) bp.message(makeMessage("!seabass says This is a really"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "seabass never said 'this is a really'") 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) { func TestBabblerSuffixSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is message one") c, k, seabass := makeMessage("This is message one")
seabass.User = &user.User{Name: "seabass"} 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" seabass.Body = "It's easier to test with unique messages"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "hi there" seabass.Body = "hi there"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-tail message one")) res = bp.message(makeMessage("!seabass says-tail message one"))
res = bp.Message(makeMessage("!seabass says-tail with unique")) res = bp.message(makeMessage("!seabass says-tail with unique"))
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "this is message one") assert.Contains(t, mb.Messages[0], "this is message one")
@ -186,16 +185,15 @@ func TestBabblerSuffixSeed(t *testing.T) {
func TestBabblerBadSuffixSeed(t *testing.T) { func TestBabblerBadSuffixSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("This is message one") c, k, seabass := makeMessage("This is message one")
seabass.User = &user.User{Name: "seabass"} 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" seabass.Body = "It's easier to test with unique messages"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "hi there" seabass.Body = "hi there"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-tail anything true")) res = bp.message(makeMessage("!seabass says-tail anything true"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "seabass never said 'anything true'") 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) { func TestBabblerBookendSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-bridge It's easier | unique messages")) res = bp.message(makeMessage("!seabass says-bridge It's easier | unique messages"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages") 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) { func TestBabblerBookendSeedShort(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-bridge It's easier to test with | unique messages")) res = bp.message(makeMessage("!seabass says-bridge It's easier to test with | unique messages"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages") 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) { func TestBabblerBadBookendSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-bridge It's easier | not unique messages")) res = bp.message(makeMessage("!seabass says-bridge It's easier | not unique messages"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "seabass never said 'it's easier ... not unique messages'") 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) { func TestBabblerMiddleOutSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-middle-out test with")) res = bp.message(makeMessage("!seabass says-middle-out test with"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "it's easier to test with unique messages") 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) { func TestBabblerBadMiddleOutSeed(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
res = bp.Message(makeMessage("!seabass says-middle-out anything true")) res = bp.message(makeMessage("!seabass says-middle-out anything true"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Equal(t, mb.Messages[0], "seabass never said 'anything true'") 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) { func TestBabblerBatch(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) 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?") 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(seabass) res := bp.message(c, k, seabass)
assert.Len(t, mb.Messages, 1) 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.Len(t, mb.Messages, 2)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[1], "this is") assert.Contains(t, mb.Messages[1], "this is")
@ -289,26 +281,25 @@ func TestBabblerBatch(t *testing.T) {
func TestBabblerMerge(t *testing.T) { func TestBabblerMerge(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
bp.config.Babbler.DefaultUsers = []string{"seabass"}
assert.NotNil(t, bp) assert.NotNil(t, bp)
seabass := makeMessage("<seabass> This is a message") c, k, seabass := makeMessage("<seabass> This is a message")
seabass.User = &user.User{Name: "seabass"} seabass.User = &user.User{Name: "seabass"}
res := bp.Message(seabass) res := bp.message(c, k, seabass)
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
seabass.Body = "<seabass> This is another message" seabass.Body = "<seabass> This is another message"
res = bp.Message(seabass) res = bp.message(c, k, seabass)
seabass.Body = "<seabass> This is a long message" seabass.Body = "<seabass> 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.True(t, res)
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "mooooiggged") assert.Contains(t, mb.Messages[0], "mooooiggged")
res = bp.Message(makeMessage("!seabass2 says")) res = bp.message(makeMessage("!seabass2 says"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
@ -320,27 +311,7 @@ func TestHelp(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
bp := newBabblerPlugin(mb) bp := newBabblerPlugin(mb)
assert.NotNil(t, bp) 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) 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())
}

View File

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"strconv" "strconv"
@ -15,6 +14,7 @@ import (
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/plugins/counter" "github.com/velour/catbase/plugins/counter"
@ -37,34 +37,33 @@ type untappdUser struct {
chanNick string chanNick string
} }
// NewBeersPlugin creates a new BeersPlugin with the Plugin interface // New BeersPlugin creates a new BeersPlugin with the Plugin interface
func New(bot bot.Bot) *BeersPlugin { func New(b bot.Bot) *BeersPlugin {
if bot.DBVersion() == 1 { if _, err := b.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,
lastCheckin integer, lastCheckin integer,
chanNick string chanNick string
);`); err != nil { );`); err != nil {
log.Fatal(err) log.Fatal().Err(err)
}
} }
p := BeersPlugin{ p := &BeersPlugin{
Bot: bot, Bot: b,
db: bot.DB(), db: b.DB(),
} }
p.LoadData() for _, channel := range b.Config().GetArray("Untappd.Channels", []string{}) {
for _, channel := range bot.Config().Untappd.Channels { go p.untappdLoop(b.DefaultConnector(), channel)
go p.untappdLoop(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. // 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. // 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. // 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) parts := strings.Fields(message.Body)
if len(parts) == 0 { if len(parts) == 0 {
@ -84,49 +83,49 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
count, err := strconv.Atoi(parts[2]) count, err := strconv.Atoi(parts[2])
if err != nil { if err != nil {
// if it's not a number, maybe it's a nick! // 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 { if count < 0 {
// you can't be negative // you can't be negative
msg := fmt.Sprintf("Sorry %s, you can't have negative beers!", nick) 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 return true
} }
if parts[1] == "+=" { if parts[1] == "+=" {
p.addBeers(nick, count) p.addBeers(nick, count)
p.randomReply(channel) p.randomReply(c, channel)
} else if parts[1] == "=" { } else if parts[1] == "=" {
if count == 0 { if count == 0 {
p.puke(nick, channel) p.puke(c, nick, channel)
} else { } else {
p.setBeers(nick, count) p.setBeers(nick, count)
p.randomReply(channel) p.randomReply(c, channel)
} }
} else { } 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 { } else if len(parts) == 2 {
if p.doIKnow(parts[1]) { if p.doIKnow(parts[1]) {
p.reportCount(parts[1], channel, false) p.reportCount(c, parts[1], channel, false)
} else { } else {
msg := fmt.Sprintf("Sorry, I don't know %s.", parts[1]) 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 { } 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 // no matter what, if we're in here, then we've responded
return true return true
} else if parts[0] == "puke" { } else if parts[0] == "puke" {
p.puke(nick, channel) p.puke(c, nick, channel)
return true return true
} }
if message.Command && parts[0] == "imbibe" { if message.Command && parts[0] == "imbibe" {
p.addBeers(nick, 1) p.addBeers(nick, 1)
p.randomReply(channel) p.randomReply(c, channel)
return true return true
} }
@ -135,7 +134,7 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel := message.Channel channel := message.Channel
if len(parts) < 2 { 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 { } else if len(parts) == 3 {
chanNick = parts[2] chanNick = parts[2]
} else if len(parts) == 4 { } else if len(parts) == 4 {
@ -148,16 +147,19 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel: channel, 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 var count int
err := p.db.QueryRow(`select count(*) from untappd err := p.db.QueryRow(`select count(*) from untappd
where untappdUser = ?`, u.untappdUser).Scan(&count) where untappdUser = ?`, u.untappdUser).Scan(&count)
if err != nil { if err != nil {
log.Println("Error registering untappd: ", err) log.Error().Err(err).Msgf("Error registering untappd")
} }
if count > 0 { 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 return true
} }
_, err = p.db.Exec(`insert into untappd ( _, err = p.db.Exec(`insert into untappd (
@ -172,45 +174,36 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
u.chanNick, u.chanNick,
) )
if err != nil { if err != nil {
log.Println("Error registering untappd: ", err) log.Error().Err(err).Msgf("Error registering untappd")
p.Bot.SendMessage(channel, "I can't see.") p.Bot.Send(c, bot.Message, channel, "I can't see.")
return true 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 return true
} }
if message.Command && parts[0] == "checkuntappd" { if message.Command && parts[0] == "checkuntappd" {
log.Println("Checking untappd at request of user.") log.Info().
p.checkUntappd(channel) Str("user", message.User.Name).
Msgf("Checking untappd at request of user.")
p.checkUntappd(c, channel)
return true return true
} }
return false 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. // 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 " + 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 " + "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!" "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 { 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) ub := getUserBeers(p.db, user)
err := ub.Update(amount) err := ub.Update(amount)
if err != nil { 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) ub := getUserBeers(p.db, user)
err := ub.UpdateDelta(delta) err := ub.UpdateDelta(delta)
if err != nil { 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 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) beers := p.getBeers(nick)
msg := fmt.Sprintf("%s has had %d beers so far.", nick, beers) msg := fmt.Sprintf("%s has had %d beers so far.", nick, beers)
if himself { 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) 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) p.setBeers(user, 0)
msg := fmt.Sprintf("Ohhhhhh, and a reversal of fortune for %s!", user) 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 { 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) // 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!"} 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 { type checkin struct {
@ -316,7 +309,12 @@ type Beers struct {
} }
func (p *BeersPlugin) pullUntappd() ([]checkin, error) { 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/" baseUrl := "https://api.untappd.com/v4/checkin/recent/"
url := baseUrl + access_token + "&limit=25" url := baseUrl + access_token + "&limit=25"
@ -332,55 +330,54 @@ func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
} }
if resp.StatusCode == 500 { 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) return []checkin{}, errors.New(resp.Status)
} }
var beers Beers var beers Beers
err = json.Unmarshal(body, &beers) err = json.Unmarshal(body, &beers)
if err != nil { if err != nil {
log.Println(err) log.Error().Err(err)
return []checkin{}, err return []checkin{}, err
} }
return beers.Response.Checkins.Items, nil return beers.Response.Checkins.Items, nil
} }
func (p *BeersPlugin) checkUntappd(channel string) { func (p *BeersPlugin) checkUntappd(c bot.Connector, channel string) {
token := p.Bot.Config().Untappd.Token token := p.Bot.Config().Get("Untappd.Token", "NONE")
if token == "" || token == "<Your Token>" { if token == "NONE" {
log.Println("No Untappd token, cannot enable plugin.") log.Info().
Msg(`Set config value "untappd.token" if you wish to enable untappd`)
return return
} }
userMap := make(map[string]untappdUser) userMap := make(map[string]untappdUser)
rows, err := p.db.Query(`select id, untappdUser, channel, lastCheckin, chanNick from untappd;`) rows, err := p.db.Query(`select id, untappdUser, channel, lastCheckin, chanNick from untappd;`)
if err != nil { if err != nil {
log.Println("Error getting untappd users: ", err) log.Error().Err(err).Msg("Error getting untappd users")
return return
} }
for rows.Next() { for rows.Next() {
u := untappdUser{} u := untappdUser{}
err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick) err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
userMap[u.untappdUser] = u userMap[u.untappdUser] = u
log.Printf("Found untappd user: %#v", u)
if u.chanNick == "" { if u.chanNick == "" {
log.Fatal("Empty chanNick for no good reason.") log.Fatal().Msg("Empty chanNick for no good reason.")
} }
} }
chks, err := p.pullUntappd() chks, err := p.pullUntappd()
if err != nil { if err != nil {
log.Println("Untappd ERROR: ", err) log.Error().Err(err).Msg("Untappd ERROR")
return return
} }
for i := len(chks); i > 0; i-- { for i := len(chks); i > 0; i-- {
checkin := chks[i-1] checkin := chks[i-1]
if checkin.Checkin_id <= userMap[checkin.User.User_name].lastCheckin { 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 continue
} }
@ -395,8 +392,9 @@ func (p *BeersPlugin) checkUntappd(channel string) {
if !ok { if !ok {
continue continue
} }
log.Printf("user.chanNick: %s, user.untappdUser: %s, checkin.User.User_name: %s", log.Debug().
user.chanNick, user.untappdUser, checkin.User.User_name) 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) p.addBeers(user.chanNick, 1)
drunken := p.getBeers(user.chanNick) drunken := p.getBeers(user.chanNick)
@ -410,11 +408,18 @@ func (p *BeersPlugin) checkUntappd(channel string) {
msg, checkin.Checkin_comment) msg, checkin.Checkin_comment)
} }
args := []interface{}{
channel,
msg,
}
if checkin.Media.Count > 0 { if checkin.Media.Count > 0 {
if strings.Contains(checkin.Media.Items[0].Photo.Photo_img_lg, "photos-processing") { if strings.Contains(checkin.Media.Items[0].Photo.Photo_img_lg, "photos-processing") {
continue 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 user.lastCheckin = checkin.Checkin_id
@ -422,33 +427,27 @@ func (p *BeersPlugin) checkUntappd(channel string) {
lastCheckin = ? lastCheckin = ?
where id = ?`, user.lastCheckin, user.id) where id = ?`, user.lastCheckin, user.id)
if err != nil { if err != nil {
log.Println("UPDATE ERROR!:", err) log.Error().Err(err).Msg("UPDATE ERROR!")
} }
log.Println("checkin id:", checkin.Checkin_id, "Message:", msg) log.Debug().
p.Bot.SendMessage(channel, msg) Int("checkin_id", checkin.Checkin_id).
Str("msg", msg).
Msg("checkin")
p.Bot.Send(c, bot.Message, args...)
} }
} }
func (p *BeersPlugin) untappdLoop(channel string) { func (p *BeersPlugin) untappdLoop(c bot.Connector, channel string) {
frequency := p.Bot.Config().Untappd.Freq 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 { for {
time.Sleep(time.Duration(frequency) * time.Second) 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 }

View File

@ -3,6 +3,7 @@
package beers package beers
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -13,12 +14,13 @@ import (
"github.com/velour/catbase/plugins/counter" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ c := &cli.CliPlugin{}
return c, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -29,18 +31,29 @@ func makeMessage(payload string) msg.Message {
func makeBeersPlugin(t *testing.T) (*BeersPlugin, *bot.MockBot) { func makeBeersPlugin(t *testing.T) (*BeersPlugin, *bot.MockBot) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
counter.New(mb) counter.New(mb)
mb.DB().MustExec(`delete from counter; delete from counter_alias;`)
b := New(mb) b := New(mb)
assert.NotNil(t, b) b.message(makeMessage("!mkalias beer :beer:"))
b.Message(makeMessage("!mkalias beer :beer:")) b.message(makeMessage("!mkalias beers :beer:"))
b.Message(makeMessage("!mkalias beers :beer:"))
return b, mb 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) { func TestImbibe(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("!imbibe")) b.message(makeMessage("!imbibe"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
b.Message(makeMessage("!imbibe")) b.message(makeMessage("!imbibe"))
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
@ -48,7 +61,7 @@ func TestImbibe(t *testing.T) {
} }
func TestEq(t *testing.T) { func TestEq(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("!beers = 3")) b.message(makeMessage("!beers = 3"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
@ -57,7 +70,7 @@ func TestEq(t *testing.T) {
func TestEqNeg(t *testing.T) { func TestEqNeg(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("!beers = -3")) b.message(makeMessage("!beers = -3"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
@ -66,8 +79,8 @@ func TestEqNeg(t *testing.T) {
func TestEqZero(t *testing.T) { func TestEqZero(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("beers += 5")) b.message(makeMessage("beers += 5"))
b.Message(makeMessage("!beers = 0")) b.message(makeMessage("!beers = 0"))
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
assert.Contains(t, mb.Messages[1], "reversal of fortune") assert.Contains(t, mb.Messages[1], "reversal of fortune")
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
@ -77,9 +90,9 @@ func TestEqZero(t *testing.T) {
func TestBeersPlusEq(t *testing.T) { func TestBeersPlusEq(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("beers += 5")) b.message(makeMessage("beers += 5"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
b.Message(makeMessage("beers += 5")) b.message(makeMessage("beers += 5"))
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
@ -88,11 +101,11 @@ func TestBeersPlusEq(t *testing.T) {
func TestPuke(t *testing.T) { func TestPuke(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("beers += 5")) b.message(makeMessage("beers += 5"))
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 5, it.Count) assert.Equal(t, 5, it.Count)
b.Message(makeMessage("puke")) b.message(makeMessage("puke"))
it, err = counter.GetItem(mb.DB(), "tester", itemName) it, err = counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, it.Count) assert.Equal(t, 0, it.Count)
@ -100,31 +113,16 @@ func TestPuke(t *testing.T) {
func TestBeersReport(t *testing.T) { func TestBeersReport(t *testing.T) {
b, mb := makeBeersPlugin(t) b, mb := makeBeersPlugin(t)
b.Message(makeMessage("beers += 5")) b.message(makeMessage("beers += 5"))
it, err := counter.GetItem(mb.DB(), "tester", itemName) it, err := counter.GetItem(mb.DB(), "tester", itemName)
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 5, it.Count) assert.Equal(t, 5, it.Count)
b.Message(makeMessage("beers")) b.message(makeMessage("beers"))
assert.Contains(t, mb.Messages[1], "5 beers") assert.Contains(t, mb.Messages[1], "5 beers")
} }
func TestHelp(t *testing.T) { func TestHelp(t *testing.T) {
b, mb := makeBeersPlugin(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) 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())
}

117
plugins/cli/cli.go Normal file
View File

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

129
plugins/cli/index.go Normal file
View File

@ -0,0 +1,129 @@
package cli
var indexHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>CLI</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>CLI</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.URL" :active="item.Name === 'CLI'">{{ "{{ item.Name }}" }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
:show="err">
{{ "{{ err }}" }}
</b-alert>
<b-container>
<b-row>
<b-col cols="5">Password:</b-col>
<b-col><b-input v-model="answer"></b-col>
</b-row>
<b-row>
<b-form-textarea
v-sticky-scroll
disabled
id="textarea"
v-model="text"
placeholder="The bot will respond here..."
rows="10"
max-rows="10"
no-resize
></b-form-textarea>
</b-row>
<b-form
@submit="send">
<b-row>
<b-col>
<b-form-input
type="text"
placeholder="Username"
v-model="user"></b-form-input>
</b-col>
<b-col>
<b-form-input
type="text"
placeholder="Enter something to send to the bot"
v-model="input"
autocomplete="off"
></b-form-input>
</b-col>
<b-col>
<b-button type="submit" :disabled="!authenticated">Send</b-button>
</b-col>
</b-row>
</b-form>
</b-container>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
err: '',
nav: {{ .Nav }},
answer: '',
correct: 0,
textarea: [],
user: '',
input: '',
},
computed: {
authenticated: function() {
if (this.user !== '')
return true;
return false;
},
text: function() {
return this.textarea.join('\n');
}
},
methods: {
addText(user, text) {
this.textarea.push(user + ": " + text);
const len = this.textarea.length;
if (this.textarea.length > 10)
this.textarea = this.textarea.slice(len-10, len);
},
send(evt) {
evt.preventDefault();
evt.stopPropagation()
if (!this.authenticated) {
return;
}
const payload = {user: this.user, payload: this.input, password: this.answer};
this.addText(this.user, this.input);
this.input = "";
axios.post('/cli/api', payload)
.then(resp => {
const data = resp.data;
this.addText(data.user, data.payload.trim());
this.err = '';
})
.catch(err => (this.err = err));
}
}
})
</script>
</body>
</html>
`

View File

@ -17,13 +17,15 @@ type CSWPlugin struct {
Config *config.Config Config *config.Config
} }
func New(bot bot.Bot) *CSWPlugin { func New(b bot.Bot) *CSWPlugin {
return &CSWPlugin{ csw := &CSWPlugin{
Bot: bot, 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 { if !message.Command {
return false 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 true
} }
return false 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
}

View File

@ -3,6 +3,7 @@
package couldashouldawoulda package couldashouldawoulda
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -29,7 +30,7 @@ func Test0(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"} possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -47,7 +48,7 @@ func Test1(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"} 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() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"} possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -83,7 +84,7 @@ func Test3(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"} possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -101,7 +102,7 @@ func Test4(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"} 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() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
possibilities := []string{"I'd say option", "You'd be an idiot not to choose the"} possibilities := []string{"I'd say option", "You'd be an idiot not to choose the"}

View File

@ -1,15 +1,18 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package counter package counter
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"log" "html/template"
"math/rand"
"net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -41,6 +44,20 @@ type alias struct {
PointsTo string `db:"points_to"` 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 // GetItems returns all counters for a subject
func GetItems(db *sqlx.DB, nick string) ([]Item, error) { func GetItems(db *sqlx.DB, nick string) ([]Item, error) {
var items []Item 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 { if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil {
itemName = a.PointsTo itemName = a.PointsTo
} else { } else {
log.Println(err, a) log.Error().Err(err).Interface("alias", a)
} }
err := db.Get(&item, `select * from counter where nick = ? and item= ?`, 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: default:
return Item{}, err 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 return item, nil
} }
@ -133,6 +154,9 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
func (i *Item) Create() error { func (i *Item) Create() error {
res, err := i.Exec(`insert into counter (nick, item, count) values (?, ?, ?);`, res, err := i.Exec(`insert into counter (nick, item, count) values (?, ?, ?);`,
i.Nick, i.Item, i.Count) i.Nick, i.Item, i.Count)
if err != nil {
return err
}
id, _ := res.LastInsertId() id, _ := res.LastInsertId()
// hackhackhack? // hackhackhack?
i.ID = id i.ID = id
@ -149,7 +173,10 @@ func (i *Item) Update(value int) error {
if i.ID == -1 { if i.ID == -1 {
i.Create() 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) _, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID)
return err return err
} }
@ -169,33 +196,35 @@ func (i *Item) Delete() error {
} }
// NewCounterPlugin creates a new CounterPlugin with the Plugin interface // NewCounterPlugin creates a new CounterPlugin with the Plugin interface
func New(bot bot.Bot) *CounterPlugin { func New(b bot.Bot) *CounterPlugin {
if _, err := bot.DB().Exec(`create table if not exists counter ( tx := b.DB().MustBegin()
b.DB().MustExec(`create table if not exists counter (
id integer primary key, id integer primary key,
nick string, nick string,
item string, item string,
count integer count integer
);`); err != nil { );`)
log.Fatal(err) b.DB().MustExec(`create table if not exists counter_alias (
}
if _, err := bot.DB().Exec(`create table if not exists counter_alias (
id integer PRIMARY KEY AUTOINCREMENT, id integer PRIMARY KEY AUTOINCREMENT,
item string NOT NULL UNIQUE, item string NOT NULL UNIQUE,
points_to string NOT NULL points_to string NOT NULL
);`); err != nil { );`)
log.Fatal(err) tx.Commit()
} cp := &CounterPlugin{
return &CounterPlugin{ Bot: b,
Bot: bot, DB: b.DB(),
DB: bot.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. // Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the // 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 // users message. Otherwise, the function returns false and the bot continues
// execution of other plugins. // 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 // This bot does not reply to anything
nick := message.User.Name nick := message.User.Name
channel := message.Channel 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 len(parts) == 3 && strings.ToLower(parts[0]) == "mkalias" {
if _, err := MkAlias(p.DB, parts[1], parts[2]); err != nil { if _, err := MkAlias(p.DB, parts[1], parts[2]); err != nil {
log.Println(err) log.Error().Err(err)
return false 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])) parts[1], parts[2]))
return true return true
} else if strings.ToLower(parts[0]) == "leaderboard" { } else if strings.ToLower(parts[0]) == "leaderboard" {
@ -226,7 +255,7 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
its, err := cmd() its, err := cmd()
if err != nil { if err != nil {
log.Println(err) log.Error().Err(err)
return false return false
} else if len(its) == 0 { } else if len(its) == 0 {
return false return false
@ -240,23 +269,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it.Item, it.Item,
) )
} }
p.Bot.SendMessage(channel, out) p.Bot.Send(c, bot.Message, channel, out)
return true return true
} else if match := teaMatcher.MatchString(message.Body); match { } else if match := teaMatcher.MatchString(message.Body); match {
// check for tea match TTT // check for tea match TTT
return p.checkMatch(message) return p.checkMatch(c, message)
} else if message.Command && message.Body == "reset me" { } else if message.Command && message.Body == "reset me" {
items, err := GetItems(p.DB, strings.ToLower(nick)) items, err := GetItems(p.DB, strings.ToLower(nick))
if err != nil { if err != nil {
log.Printf("Error getting items to reset %s: %s", nick, err) log.Error().
p.Bot.SendMessage(channel, "Something is technically wrong with your counters.") 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 return true
} }
log.Printf("Items: %+v", items) log.Debug().Msgf("Items: %+v", items)
for _, item := range items { for _, item := range items {
item.Delete() 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 return true
} else if message.Command && parts[0] == "inspect" && len(parts) == 2 { } else if message.Command && parts[0] == "inspect" && len(parts) == 2 {
var subject string var subject string
@ -267,12 +299,17 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
subject = strings.ToLower(parts[1]) 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" // pull all of the items associated with "subject"
items, err := GetItems(p.DB, subject) items, err := GetItems(p.DB, subject)
if err != nil { if err != nil {
log.Fatalf("Error retrieving items for %s: %s", subject, err) log.Error().
p.Bot.SendMessage(channel, "Something went wrong finding that counter;") Err(err).
Str("subject", subject).
Msg("Error retrieving items")
p.Bot.Send(c, bot.Message, channel, "Something went wrong finding that counter;")
return true return true
} }
@ -292,11 +329,11 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
resp += "." resp += "."
if count == 0 { 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 return true
} }
p.Bot.SendMessage(channel, resp) p.Bot.Send(c, bot.Message, channel, resp)
return true return true
} else if message.Command && len(parts) == 2 && parts[0] == "clear" { } else if message.Command && len(parts) == 2 && parts[0] == "clear" {
subject := strings.ToLower(nick) subject := strings.ToLower(nick)
@ -304,18 +341,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it, err := GetItem(p.DB, subject, itemName) it, err := GetItem(p.DB, subject, itemName)
if err != nil { if err != nil {
log.Printf("Error getting item to remove %s.%s: %s", subject, itemName, err) log.Error().
p.Bot.SendMessage(channel, "Something went wrong removing that counter;") 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 return true
} }
err = it.Delete() err = it.Delete()
if err != nil { if err != nil {
log.Printf("Error removing item %s.%s: %s", subject, itemName, err) log.Error().
p.Bot.SendMessage(channel, "Something went wrong removing that counter;") 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 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)) itemName))
return true return true
@ -338,16 +383,19 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
item, err := GetItem(p.DB, subject, itemName) item, err := GetItem(p.DB, subject, itemName)
switch { switch {
case err == sql.ErrNoRows: 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)) subject, itemName))
return true return true
case err != nil: case err != nil:
log.Printf("Error retrieving item count for %s.%s: %s", log.Error().
subject, itemName, err) Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error retrieving item count")
return true 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)) itemName))
return true return true
@ -372,25 +420,33 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// ++ those fuckers // ++ those fuckers
item, err := GetItem(p.DB, subject, itemName) item, err := GetItem(p.DB, subject, itemName)
if err != nil { 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 // Item ain't there, I guess
return false return false
} }
log.Printf("About to update item: %#v", item) log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1) 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)) item.Count, item.Item))
return true return true
} else if strings.HasSuffix(parts[0], "--") { } else if strings.HasSuffix(parts[0], "--") {
// -- those fuckers // -- those fuckers
item, err := GetItem(p.DB, subject, itemName) item, err := GetItem(p.DB, subject, itemName)
if err != nil { 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 // Item ain't there, I guess
return false return false
} }
item.UpdateDelta(-1) 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)) item.Count, item.Item))
return true return true
} }
@ -412,28 +468,36 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// += those fuckers // += those fuckers
item, err := GetItem(p.DB, subject, itemName) item, err := GetItem(p.DB, subject, itemName)
if err != nil { 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 // Item ain't there, I guess
return false return false
} }
n, _ := strconv.Atoi(parts[2]) 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) 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)) item.Count, item.Item))
return true return true
} else if parts[1] == "-=" { } else if parts[1] == "-=" {
// -= those fuckers // -= those fuckers
item, err := GetItem(p.DB, subject, itemName) item, err := GetItem(p.DB, subject, itemName)
if err != nil { 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 // Item ain't there, I guess
return false return false
} }
n, _ := strconv.Atoi(parts[2]) 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) 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)) item.Count, item.Item))
return true 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *CounterPlugin) Help(channel string, parts []string) { func (p *CounterPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "You can set counters incrementally by using "+ p.Bot.Send(c, bot.Message, message.Channel, "You can set counters incrementally by using "+
"<noun>++ and <noun>--. You can see all of your counters using "+ "<noun>++ and <noun>--. You can see all of your counters using "+
"\"inspect\", erase them with \"clear\", and view single counters with "+ "\"inspect\", erase them with \"clear\", and view single counters with "+
"\"count\".") "\"count\".")
return true
} }
// Empty event handler because this plugin does not do anything on event recv func (p *CounterPlugin) checkMatch(c bot.Connector, message msg.Message) bool {
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 {
nick := message.User.Name nick := message.User.Name
channel := message.Channel channel := message.Channel
@ -480,13 +528,98 @@ func (p *CounterPlugin) checkMatch(message msg.Message) bool {
// We will specifically allow :tea: to keep compatability // We will specifically allow :tea: to keep compatability
item, err := GetItem(p.DB, nick, itemName) item, err := GetItem(p.DB, nick, itemName)
if err != nil || (item.Count == 0 && item.Item != ":tea:") { 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 // Item ain't there, I guess
return false return false
} }
log.Printf("About to update item: %#v", item) log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1) item.UpdateDelta(1)
p.Bot.SendMessage(channel, fmt.Sprintf("bleep-bloop-blop... %s has %d %s", p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s... %s has %d %s",
nick, item.Count, itemName)) strings.Join(everyDayImShuffling([]string{"bleep", "bloop", "blop"}), "-"), nick, item.Count, itemName))
return true 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))
}

View File

@ -4,6 +4,7 @@ package counter
import ( import (
"fmt" "fmt"
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -16,17 +17,18 @@ import (
func setup(t *testing.T) (*bot.MockBot, *CounterPlugin) { func setup(t *testing.T) (*bot.MockBot, *CounterPlugin) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
mb.DB().MustExec(`delete from counter; delete from counter_alias;`)
_, err := MkAlias(mb.DB(), "tea", ":tea:") _, err := MkAlias(mb.DB(), "tea", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
return mb, c return mb, c
} }
func makeMessage(payload string) msg.Message { func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -37,8 +39,8 @@ func makeMessage(payload string) msg.Message {
func TestThreeSentencesExists(t *testing.T) { func TestThreeSentencesExists(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage(":beer:++")) c.message(makeMessage(":beer:++"))
c.Message(makeMessage(":beer:. Earl Grey. Hot.")) c.message(makeMessage(":beer:. Earl Grey. Hot."))
item, err := GetItem(mb.DB(), "tester", ":beer:") item, err := GetItem(mb.DB(), "tester", ":beer:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, item.Count) assert.Equal(t, 2, item.Count)
@ -48,7 +50,7 @@ func TestThreeSentencesNotExists(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
item, err := GetItem(mb.DB(), "tester", ":beer:") 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:") item, err = GetItem(mb.DB(), "tester", ":beer:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, item.Count) assert.Equal(t, 0, item.Count)
@ -57,8 +59,8 @@ func TestThreeSentencesNotExists(t *testing.T) {
func TestTeaEarlGreyHot(t *testing.T) { func TestTeaEarlGreyHot(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) 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:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, item.Count) assert.Equal(t, 2, item.Count)
@ -67,8 +69,8 @@ func TestTeaEarlGreyHot(t *testing.T) {
func TestTeaTwoPeriods(t *testing.T) { func TestTeaTwoPeriods(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) 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:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, item.Count) assert.Equal(t, 0, item.Count)
@ -77,8 +79,8 @@ func TestTeaTwoPeriods(t *testing.T) {
func TestTeaMultiplePeriods(t *testing.T) { func TestTeaMultiplePeriods(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) 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:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 2, item.Count) assert.Equal(t, 2, item.Count)
@ -87,9 +89,9 @@ func TestTeaMultiplePeriods(t *testing.T) {
func TestTeaGreenHot(t *testing.T) { func TestTeaGreenHot(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage("Tea. Green. Hot.")) c.message(makeMessage("Tea. Green. Hot."))
c.Message(makeMessage("Tea. Green. Hot")) c.message(makeMessage("Tea. Green. Hot"))
c.Message(makeMessage("Tea. Green. Iced.")) c.message(makeMessage("Tea. Green. Iced."))
item, err := GetItem(mb.DB(), "tester", ":tea:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 3, item.Count) assert.Equal(t, 3, item.Count)
@ -98,8 +100,8 @@ func TestTeaGreenHot(t *testing.T) {
func TestTeaUnrelated(t *testing.T) { func TestTeaUnrelated(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage("Tea.")) c.message(makeMessage("Tea."))
c.Message(makeMessage("Tea. It's great.")) c.message(makeMessage("Tea. It's great."))
item, err := GetItem(mb.DB(), "tester", ":tea:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, item.Count) assert.Equal(t, 0, item.Count)
@ -108,7 +110,7 @@ func TestTeaUnrelated(t *testing.T) {
func TestTeaSkieselQuote(t *testing.T) { func TestTeaSkieselQuote(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) 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:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 0, item.Count) assert.Equal(t, 0, item.Count)
@ -116,7 +118,7 @@ func TestTeaSkieselQuote(t *testing.T) {
func TestTeaUnicodeJapanese(t *testing.T) { func TestTeaUnicodeJapanese(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage("Tea. おちや. Hot.")) c.message(makeMessage("Tea. おちや. Hot."))
item, err := GetItem(mb.DB(), "tester", ":tea:") item, err := GetItem(mb.DB(), "tester", ":tea:")
assert.Nil(t, err) assert.Nil(t, err)
assert.Equal(t, 1, item.Count) assert.Equal(t, 1, item.Count)
@ -125,8 +127,8 @@ func TestTeaUnicodeJapanese(t *testing.T) {
func TestResetMe(t *testing.T) { func TestResetMe(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage("test++")) c.message(makeMessage("test++"))
c.Message(makeMessage("!reset me")) c.message(makeMessage("!reset me"))
items, err := GetItems(mb.DB(), "tester") items, err := GetItems(mb.DB(), "tester")
assert.Nil(t, err) assert.Nil(t, err)
assert.Len(t, items, 0) assert.Len(t, items, 0)
@ -135,7 +137,7 @@ func TestResetMe(t *testing.T) {
func TestCounterOne(t *testing.T) { func TestCounterOne(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage("test++")) c.message(makeMessage("test++"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Equal(t, mb.Messages[0], "tester has 1 test.") assert.Equal(t, mb.Messages[0], "tester has 1 test.")
} }
@ -143,7 +145,7 @@ func TestCounterOne(t *testing.T) {
func TestCounterOneWithSpace(t *testing.T) { func TestCounterOneWithSpace(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
c.Message(makeMessage(":test: ++")) c.message(makeMessage(":test: ++"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Equal(t, mb.Messages[0], "tester has 1 :test:.") assert.Equal(t, mb.Messages[0], "tester has 1 :test:.")
} }
@ -152,7 +154,7 @@ func TestCounterFour(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
c.Message(makeMessage("test++")) c.message(makeMessage("test++"))
} }
assert.Len(t, mb.Messages, 4) assert.Len(t, mb.Messages, 4)
assert.Equal(t, mb.Messages[3], "tester has 4 test.") assert.Equal(t, mb.Messages[3], "tester has 4 test.")
@ -162,10 +164,10 @@ func TestCounterDecrement(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) 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.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "tester has 3 test.") assert.Equal(t, mb.Messages[4], "tester has 3 test.")
} }
@ -174,10 +176,10 @@ func TestFriendCounterDecrement(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) 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.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "other has 3 test.") assert.Equal(t, mb.Messages[4], "other has 3 test.")
} }
@ -186,12 +188,12 @@ func TestDecrementZero(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
} }
j := 4 j := 4
for i := 4; i > 0; i-- { 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)) assert.Equal(t, mb.Messages[j], fmt.Sprintf("tester has %d test.", i-1))
j++ j++
} }
@ -203,10 +205,10 @@ func TestClear(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) 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.True(t, res)
assert.Len(t, mb.Actions, 1) assert.Len(t, mb.Actions, 1)
assert.Equal(t, mb.Actions[0], "chops a few test out of his brain") 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) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) 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.True(t, res)
assert.Len(t, mb.Messages, 5) assert.Len(t, mb.Messages, 5)
assert.Equal(t, mb.Messages[4], "tester has 4 test.") assert.Equal(t, mb.Messages[4], "tester has 4 test.")
@ -229,18 +231,18 @@ func TestInspectMe(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 4; i++ { 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)) assert.Equal(t, mb.Messages[i], fmt.Sprintf("tester has %d test.", i+1))
} }
for i := 0; i < 2; i++ { 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)) assert.Equal(t, mb.Messages[i+4], fmt.Sprintf("tester has %d fucks.", i+1))
} }
for i := 0; i < 20; i++ { 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)) 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.True(t, res)
assert.Len(t, mb.Messages, 27) assert.Len(t, mb.Messages, 27)
assert.Equal(t, mb.Messages[26], "tester has the following counters: test: 4, fucks: 2, cheese: 20.") 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) { func TestHelp(t *testing.T) {
mb, c := setup(t) mb, c := setup(t)
assert.NotNil(t, c) 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) 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())
}

103
plugins/counter/html.go Normal file
View File

@ -0,0 +1,103 @@
package counter
var html = `
<html>
<head>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<title>Counters</title>
</head>
<body>
<div id="app">
<b-navbar>
<b-navbar-brand>Counters</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.URL" :active="item.Name === 'Counter'">{{ "{{ item.Name }}" }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
:show="err"
variant="error">
{{ "{{ err }}" }}
</b-alert>
<b-container>
<b-row>
<b-col cols="5">Password:</b-col>
<b-col><b-input v-model="answer"></b-col>
</b-row>
<b-row v-for="(counter, user) in counters">
{{ "{{ user }}" }}:
<b-container>
<b-row v-for="(count, thing) in counter">
<b-col offset="1">
{{ "{{ thing }}" }}:
</b-col>
<b-col>
{{ "{{ count }}" }}
</b-col>
<b-col cols="2">
<button @click="subtract(user,thing,count)">-</button>
<button @click="add(user,thing,count)">+</button>
</b-col>
</b-row>
</b-container>
</b-row>
</b-container>
</div>
<script>
function convertData(data) {
var newData = {};
for (let i = 0; i < data.length; i++) {
let entry = data[i]
if (newData[entry.Nick] === undefined) {
newData[entry.Nick] = {}
}
newData[entry.Nick][entry.Item] = entry.Count;
}
return newData;
}
var app = new Vue({
el: '#app',
data: {
err: '',
nav: {{ .Nav }},
answer: '',
correct: 0,
counters: {}
},
mounted() {
axios.get('/counter/api')
.then(resp => (this.counters = convertData(resp.data)))
.catch(err => (this.err = err));
},
methods: {
add(user, thing, count) {
axios.post('/counter/api',
{user: user, thing: thing, action: '++', password: this.answer})
.then(resp => {this.counters = convertData(resp.data); this.err = '';})
.catch(err => this.err = err);
},
subtract(user, thing, count) {
axios.post('/counter/api',
{user: user, thing: thing, action: '--', password: this.answer})
.then(resp => {this.counters = convertData(resp.data); this.err = '';})
.catch(err => this.err = err);
}
}
})
</script>
</body>
</html>
`

View File

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

View File

@ -19,10 +19,13 @@ type DicePlugin struct {
} }
// NewDicePlugin creates a new DicePlugin with the Plugin interface // NewDicePlugin creates a new DicePlugin with the Plugin interface
func New(bot bot.Bot) *DicePlugin { func New(b bot.Bot) *DicePlugin {
return &DicePlugin{ dp := &DicePlugin{
Bot: bot, Bot: b,
} }
b.Register(dp, bot.Message, dp.message)
b.Register(dp, bot.Help, dp.help)
return dp
} }
func rollDie(sides int) int { func rollDie(sides int) int {
@ -32,7 +35,7 @@ func rollDie(sides int) int {
// Message responds to the bot hook on recieving messages. // 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. // 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. // 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 { if !message.Command {
return false return false
} }
@ -46,7 +49,7 @@ func (p *DicePlugin) Message(message msg.Message) bool {
} }
if sides < 2 || nDice < 1 || nDice > 20 { 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 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 return true
} }
// 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 *DicePlugin) Help(channel string, parts []string) { func (p *DicePlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Roll dice using notation XdY. Try \"3d20\".") 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 }

View File

@ -3,6 +3,7 @@
package dice package dice
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -29,7 +30,7 @@ func TestDie(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!1d6")) res := c.message(makeMessage("!1d6"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "tester, you rolled:") assert.Contains(t, mb.Messages[0], "tester, you rolled:")
@ -39,7 +40,7 @@ func TestDice(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!5d6")) res := c.message(makeMessage("!5d6"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "tester, you rolled:") assert.Contains(t, mb.Messages[0], "tester, you rolled:")
@ -49,7 +50,7 @@ func TestNotCommand(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("1d6")) res := c.message(makeMessage("1d6"))
assert.False(t, res) assert.False(t, res)
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
} }
@ -58,7 +59,7 @@ func TestBadDice(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!aued6")) res := c.message(makeMessage("!aued6"))
assert.False(t, res) assert.False(t, res)
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
} }
@ -67,7 +68,7 @@ func TestBadSides(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!1daoeu")) res := c.message(makeMessage("!1daoeu"))
assert.False(t, res) assert.False(t, res)
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
} }
@ -76,7 +77,7 @@ func TestLotsOfDice(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!100d100")) res := c.message(makeMessage("!100d100"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "You're a dick.") assert.Contains(t, mb.Messages[0], "You're a dick.")
@ -86,27 +87,6 @@ func TestHelp(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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) 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

@ -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 <nick>, 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 <nick>\"")
}
// 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 }

View File

@ -5,11 +5,12 @@ package emojifyme
import ( import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"strings" "strings"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -20,15 +21,15 @@ type EmojifyMePlugin struct {
Emoji map[string]string 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") resp, err := http.Get("https://raw.githubusercontent.com/github/gemoji/master/db/emoji.json")
if err != nil { 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) body, err := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
if err != nil { 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 { type Emoji struct {
@ -38,7 +39,7 @@ func New(bot bot.Bot) *EmojifyMePlugin {
var emoji []Emoji var emoji []Emoji
err = json.Unmarshal(body, &emoji) err = json.Unmarshal(body, &emoji)
if err != nil { if err != nil {
log.Fatalf("Error parsing emoji list: %s", err) log.Fatal().Err(err).Msg("Error parsing emoji list")
} }
emojiMap := map[string]string{} emojiMap := map[string]string{}
@ -48,14 +49,16 @@ func New(bot bot.Bot) *EmojifyMePlugin {
} }
} }
return &EmojifyMePlugin{ ep := &EmojifyMePlugin{
Bot: bot, Bot: b,
GotBotEmoji: false, GotBotEmoji: false,
Emoji: emojiMap, 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 { if !p.GotBotEmoji {
p.GotBotEmoji = true p.GotBotEmoji = true
emojiMap := p.Bot.GetEmojiList() 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 emojied := 0.0
tokens := strings.Fields(strings.ToLower(message.Body)) emojys := []string{}
for i, token := range tokens { msg := strings.Replace(strings.ToLower(message.Body), "_", " ", -1)
if _, ok := p.Emoji[token]; ok { for k, v := range p.Emoji {
if !stringsContain(inertTokens, token) { k = strings.Replace(k, "_", " ", -1)
emojied++ candidates := []string{
} k,
tokens[i] = ":" + token + ":" k + "es",
} else if strings.HasSuffix(token, "s") { k + "s",
//Check to see if we can strip the trailing "s" off and get an emoji }
temp := strings.TrimSuffix(token, "s") for _, c := range candidates {
if _, ok := p.Emoji[temp]; ok { if strings.Contains(msg, " "+c+" ") ||
if !stringsContain(inertTokens, temp) { strings.HasPrefix(msg, c+" ") ||
emojied++ strings.HasSuffix(msg, " "+c) ||
} msg == c {
tokens[i] = ":" + temp + ":s" emojys = append(emojys, v)
} else if strings.HasSuffix(token, "es") { if !stringsContain(inertTokens, k) && len(v) > 2 {
//Check to see if we can strip the trailing "es" off and get an emoji emojied += 1
temp := strings.TrimSuffix(token, "es")
if _, ok := p.Emoji[temp]; ok {
if !stringsContain(inertTokens, temp) {
emojied++
}
tokens[i] = ":" + temp + ":es"
} }
} }
} }
} }
if emojied > 0 && rand.Float64() <= p.Bot.Config().Emojify.Chance*emojied {
modified := strings.Join(tokens, " ") if emojied > 0 && rand.Float64() <= p.Bot.Config().GetFloat64("Emojify.Chance", 0.02)*emojied {
p.Bot.SendMessage(message.Channel, modified) for _, e := range emojys {
return true p.Bot.Send(c, bot.Reaction, message.Channel, e, message)
}
return false
} }
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 { func stringsContain(haystack []string, needle string) bool {
for _, s := range haystack { for _, s := range haystack {
if s == needle { if s == needle {

60
plugins/fact/fact_test.go Normal file
View File

@ -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 <react> 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 <react> 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")
}

View File

@ -4,15 +4,17 @@ package fact
import ( import (
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"log"
"math/rand" "math/rand"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -22,14 +24,14 @@ import (
// respond to queries in a way that is unpredictable and fun // respond to queries in a way that is unpredictable and fun
// factoid stores info about our factoid for lookup and later interaction // factoid stores info about our factoid for lookup and later interaction
type factoid struct { type Factoid struct {
id sql.NullInt64 ID sql.NullInt64
Fact string Fact string
Tidbit string Tidbit string
Verb string Verb string
Owner string Owner string
created time.Time Created time.Time
accessed time.Time Accessed time.Time
Count int Count int
} }
@ -38,14 +40,14 @@ type alias struct {
Next string 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 // perform DB query to fill the To field
q := `select fact, next from factoid_alias where fact=?` q := `select fact, next from factoid_alias where fact=?`
var next alias var next alias
err := db.Get(&next, q, a.Next) err := db.Get(&next, q, a.Next)
if err != nil { if err != nil {
// we hit the end of the chain, get a factoid named Next // 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 { if err != nil {
err := fmt.Errorf("Error resolvig alias %v: %v", a, err) err := fmt.Errorf("Error resolvig alias %v: %v", a, err)
return nil, err return nil, err
@ -55,7 +57,7 @@ func (a *alias) resolve(db *sqlx.DB) (*factoid, error) {
return next.resolve(db) 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=?` q := `select * from factoid_alias where fact=?`
var a alias var a alias
err := db.Get(&a, q, fact) err := db.Get(&a, q, fact)
@ -89,9 +91,9 @@ func aliasFromStrings(from, to string) *alias {
return &alias{from, to} return &alias{from, to}
} }
func (f *factoid) save(db *sqlx.DB) error { func (f *Factoid) Save(db *sqlx.DB) error {
var err error var err error
if f.id.Valid { if f.ID.Valid {
// update // update
_, err = db.Exec(`update factoid set _, err = db.Exec(`update factoid set
fact=?, fact=?,
@ -105,12 +107,12 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit, f.Tidbit,
f.Verb, f.Verb,
f.Owner, f.Owner,
f.accessed.Unix(), f.Accessed.Unix(),
f.Count, f.Count,
f.id.Int64) f.ID.Int64)
} else { } else {
f.created = time.Now() f.Created = time.Now()
f.accessed = time.Now() f.Accessed = time.Now()
// insert // insert
res, err := db.Exec(`insert into factoid ( res, err := db.Exec(`insert into factoid (
fact, fact,
@ -125,8 +127,8 @@ func (f *factoid) save(db *sqlx.DB) error {
f.Tidbit, f.Tidbit,
f.Verb, f.Verb,
f.Owner, f.Owner,
f.created.Unix(), f.Created.Unix(),
f.accessed.Unix(), f.Accessed.Unix(),
f.Count, f.Count,
) )
if err != nil { if err != nil {
@ -134,23 +136,23 @@ func (f *factoid) save(db *sqlx.DB) error {
} }
id, err := res.LastInsertId() id, err := res.LastInsertId()
// hackhackhack? // hackhackhack?
f.id.Int64 = id f.ID.Int64 = id
f.id.Valid = true f.ID.Valid = true
} }
return err return err
} }
func (f *factoid) delete(db *sqlx.DB) error { func (f *Factoid) delete(db *sqlx.DB) error {
var err error var err error
if f.id.Valid { if f.ID.Valid {
_, err = db.Exec(`delete from factoid where id=?`, f.id) _, err = db.Exec(`delete from factoid where id=?`, f.ID)
} }
f.id.Valid = false f.ID.Valid = false
return err return err
} }
func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) { func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*Factoid, error) {
var fs []*factoid var fs []*Factoid
query := `select query := `select
id, id,
fact, fact,
@ -166,15 +168,15 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
rows, err := db.Query(query, rows, err := db.Query(query,
"%"+fact+"%", "%"+tidbit+"%") "%"+fact+"%", "%"+tidbit+"%")
if err != nil { if err != nil {
log.Printf("Error regexping for facts: %s", err) log.Error().Err(err).Msg("Error regexping for facts")
return nil, err return nil, err
} }
for rows.Next() { for rows.Next() {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := rows.Scan( err := rows.Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -186,15 +188,15 @@ func getFacts(db *sqlx.DB, fact string, tidbit string) ([]*factoid, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
fs = append(fs, &f) fs = append(fs, &f)
} }
return fs, err return fs, err
} }
func getSingle(db *sqlx.DB) (*factoid, error) { func GetSingle(db *sqlx.DB) (*Factoid, error) {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := db.QueryRow(`select err := db.QueryRow(`select
@ -208,7 +210,7 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
count count
from factoid from factoid
order by random() limit 1;`).Scan( order by random() limit 1;`).Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -217,13 +219,13 @@ func getSingle(db *sqlx.DB) (*factoid, error) {
&tmpAccessed, &tmpAccessed,
&f.Count, &f.Count,
) )
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err return &f, err
} }
func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) { func GetSingleFact(db *sqlx.DB, fact string) (*Factoid, error) {
var f factoid var f Factoid
var tmpCreated int64 var tmpCreated int64
var tmpAccessed int64 var tmpAccessed int64
err := db.QueryRow(`select err := db.QueryRow(`select
@ -239,7 +241,7 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
where fact like ? where fact like ?
order by random() limit 1;`, order by random() limit 1;`,
fact).Scan( fact).Scan(
&f.id, &f.ID,
&f.Fact, &f.Fact,
&f.Tidbit, &f.Tidbit,
&f.Verb, &f.Verb,
@ -248,22 +250,22 @@ func getSingleFact(db *sqlx.DB, fact string) (*factoid, error) {
&tmpAccessed, &tmpAccessed,
&f.Count, &f.Count,
) )
f.created = time.Unix(tmpCreated, 0) f.Created = time.Unix(tmpCreated, 0)
f.accessed = time.Unix(tmpAccessed, 0) f.Accessed = time.Unix(tmpAccessed, 0)
return &f, err return &f, err
} }
// Factoid provides the necessary plugin-wide needs // Factoid provides the necessary plugin-wide needs
type Factoid struct { type FactoidPlugin struct {
Bot bot.Bot Bot bot.Bot
NotFound []string NotFound []string
LastFact *factoid LastFact *Factoid
db *sqlx.DB db *sqlx.DB
} }
// NewFactoid creates a new Factoid with the Plugin interface // NewFactoid creates a new Factoid with the Plugin interface
func New(botInst bot.Bot) *Factoid { func New(botInst bot.Bot) *FactoidPlugin {
p := &Factoid{ p := &FactoidPlugin{
Bot: botInst, Bot: botInst,
NotFound: []string{ NotFound: []string{
"I don't know.", "I don't know.",
@ -276,6 +278,8 @@ func New(botInst bot.Bot) *Factoid {
db: botInst.DB(), db: botInst.DB(),
} }
c := botInst.DefaultConnector()
if _, err := p.db.Exec(`create table if not exists factoid ( if _, err := p.db.Exec(`create table if not exists factoid (
id integer primary key, id integer primary key,
fact string, fact string,
@ -286,7 +290,7 @@ func New(botInst bot.Bot) *Factoid {
accessed integer, accessed integer,
count integer count integer
);`); err != nil { );`); err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
if _, err := p.db.Exec(`create table if not exists factoid_alias ( if _, err := p.db.Exec(`create table if not exists factoid_alias (
@ -294,17 +298,17 @@ func New(botInst bot.Bot) *Factoid {
next string, next string,
primary key (fact, next) primary key (fact, next)
);`); err != nil { );`); err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
for _, channel := range botInst.Config().Channels { for _, channel := range botInst.Config().GetArray("channels", []string{}) {
go p.factTimer(channel) go p.factTimer(c, 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().Factoid.StartupFact); ok { if ok, fact := p.findTrigger(p.Bot.Config().Get("Factoid.StartupFact", "speed test")); ok {
p.sayFact(msg.Message{ p.sayFact(c, msg.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
Command: true, Command: true,
@ -314,6 +318,11 @@ func New(botInst bot.Bot) *Factoid {
}(channel) }(channel)
} }
botInst.Register(p, bot.Message, p.message)
botInst.Register(p, bot.Help, p.help)
p.registerWeb()
return p return p
} }
@ -338,45 +347,53 @@ func findAction(message string) string {
// learnFact assumes we have a learning situation and inserts a new fact // learnFact assumes we have a learning situation and inserts a new fact
// into the database // 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) 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 var count sql.NullInt64
err := p.db.QueryRow(`select count(*) from factoid err := p.db.QueryRow(`select count(*) from factoid
where fact=? and verb=? and tidbit=?`, where fact=? and verb=? and tidbit=?`,
fact, verb, tidbit).Scan(&count) fact, verb, tidbit).Scan(&count)
if err != nil { if err != nil {
log.Println("Error counting facts: ", err) log.Error().Err(err).Msg("Error counting facts")
return false return fmt.Errorf("What?")
} else if count.Valid && count.Int64 != 0 { } else if count.Valid && count.Int64 != 0 {
log.Println("User tried to relearn a fact.") log.Debug().Msg("User tried to relearn a fact.")
return false return fmt.Errorf("Look, I already know that.")
} }
n := factoid{ n := Factoid{
Fact: fact, Fact: fact,
Tidbit: tidbit, Tidbit: tidbit,
Verb: verb, Verb: verb,
Owner: message.User.Name, Owner: message.User.Name,
created: time.Now(), Created: time.Now(),
accessed: time.Now(), Accessed: time.Now(),
Count: 0, Count: 0,
} }
p.LastFact = &n p.LastFact = &n
err = n.save(p.db) err = n.Save(p.db)
if err != nil { if err != nil {
log.Println("Error inserting fact: ", err) log.Error().Err(err).Msg("Error inserting fact")
return false return fmt.Errorf("My brain is overheating.")
} }
return true return nil
} }
// findTrigger checks to see if a given string is a trigger or not // 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 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 { if err != nil {
return findAlias(p.db, fact) 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 // sayFact spits out a fact to the channel and updates the fact in the database
// with new time and count information // 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) msg := p.Bot.Filter(message, fact.Tidbit)
full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s", full := p.Bot.Filter(message, fmt.Sprintf("%s %s %s",
fact.Fact, fact.Verb, fact.Tidbit, fact.Fact, fact.Verb, fact.Tidbit,
@ -397,39 +414,42 @@ func (p *Factoid) sayFact(message msg.Message, fact factoid) {
} }
if fact.Verb == "action" { 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" { } else if fact.Verb == "reply" {
p.Bot.SendMessage(message.Channel, msg) p.Bot.Send(c, bot.Message, message.Channel, msg)
} else { } else {
p.Bot.SendMessage(message.Channel, full) p.Bot.Send(c, bot.Message, message.Channel, full)
} }
} }
// update fact tracking // update fact tracking
fact.accessed = time.Now() fact.Accessed = time.Now()
fact.Count += 1 fact.Count += 1
err := fact.save(p.db) err := fact.Save(p.db)
if err != nil { if err != nil {
log.Printf("Could not update fact.\n") log.Error().
log.Printf("%#v\n", fact) Interface("fact", fact).
log.Println(err) Err(err).
Msg("could not update fact")
} }
p.LastFact = &fact p.LastFact = &fact
} }
// trigger checks the message for its fitness to be a factoid and then hauls // 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 // the message off to sayFact for processing if it is in fact a trigger
func (p *Factoid) trigger(message msg.Message) bool { func (p *FactoidPlugin) trigger(c bot.Connector, message msg.Message) bool {
minLen := p.Bot.Config().Factoid.MinLen minLen := p.Bot.Config().GetInt("Factoid.MinLen", 4)
if len(message.Body) > minLen || message.Command || message.Body == "..." { if len(message.Body) > minLen || message.Command || message.Body == "..." {
if ok, fact := p.findTrigger(message.Body); ok { if ok, fact := p.findTrigger(message.Body); ok {
p.sayFact(message, *fact) p.sayFact(c, message, *fact)
return true return true
} }
r := strings.NewReplacer("'", "", "\"", "", ",", "", ".", "", ":", "", r := strings.NewReplacer("'", "", "\"", "", ",", "", ".", "", ":", "",
"?", "", "!", "") "?", "", "!", "")
if ok, fact := p.findTrigger(r.Replace(message.Body)); ok { if ok, fact := p.findTrigger(r.Replace(message.Body)); ok {
p.sayFact(message, *fact) p.sayFact(c, message, *fact)
return true return true
} }
} }
@ -438,20 +458,20 @@ func (p *Factoid) trigger(message msg.Message) bool {
} }
// tellThemWhatThatWas is a hilarious name for a function. // 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 fact := p.LastFact
var msg string var msg string
if fact == nil { if fact == nil {
msg = "Nope." msg = "Nope."
} else { } else {
msg = fmt.Sprintf("That was (#%d) '%s <%s> %s'", 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 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 body := message.Body
parts := strings.SplitN(body, action, 2) parts := strings.SplitN(body, action, 2)
@ -465,21 +485,21 @@ func (p *Factoid) learnAction(message msg.Message, action string) bool {
action = strings.TrimSpace(action) action = strings.TrimSpace(action)
if len(trigger) == 0 || len(fact) == 0 || len(action) == 0 { 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 return true
} }
if len(strings.Split(fact, "$and")) > 4 { 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 return true
} }
strippedaction := strings.Replace(strings.Replace(action, "<", "", 1), ">", "", 1) strippedaction := strings.Replace(strings.Replace(action, "<", "", 1), ">", "", 1)
if p.learnFact(message, trigger, strippedaction, fact) { if err := p.learnFact(message, trigger, strippedaction, fact); err != nil {
p.Bot.SendMessage(message.Channel, fmt.Sprintf("Okay, %s.", message.User.Name)) p.Bot.Send(c, bot.Message, message.Channel, err.Error())
} else { } 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 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 // If the user requesting forget is either the owner of the last learned fact or
// an admin, it may be deleted // 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 { if p.LastFact == nil {
p.Bot.SendMessage(message.Channel, "I refuse.") p.Bot.Send(c, bot.Message, message.Channel, "I refuse.")
return true return true
} }
err := p.LastFact.delete(p.db) err := p.LastFact.delete(p.db)
if err != nil { 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.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 p.LastFact = nil
return true return true
} }
// Allow users to change facts with a simple regexp // 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) oper := changeOperator(message.Body)
parts := strings.SplitN(message.Body, oper, 2) parts := strings.SplitN(message.Body, oper, 2)
userexp := strings.TrimSpace(parts[1]) userexp := strings.TrimSpace(parts[1])
@ -524,12 +547,16 @@ func (p *Factoid) changeFact(message msg.Message) bool {
parts = strings.Split(userexp, "/") 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 { if len(parts) == 4 {
// replacement // replacement
if parts[0] != "s" { if parts[0] != "s" {
p.Bot.SendMessage(message.Channel, "Nah.") p.Bot.Send(c, bot.Message, message.Channel, "Nah.")
} }
find := parts[1] find := parts[1]
replace := parts[2] replace := parts[2]
@ -537,17 +564,20 @@ func (p *Factoid) changeFact(message msg.Message) bool {
// replacement // replacement
result, err := getFacts(p.db, trigger, parts[1]) result, err := getFacts(p.db, trigger, parts[1])
if err != nil { 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' { if userexp[len(userexp)-1] != 'g' {
result = result[:1] result = result[:1]
} }
// make the changes // make the changes
msg := fmt.Sprintf("Changing %d facts.", len(result)) 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) reg, err := regexp.Compile(find)
if err != nil { 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 return false
} }
for _, fact := range result { for _, fact := range result {
@ -556,27 +586,30 @@ func (p *Factoid) changeFact(message msg.Message) bool {
fact.Verb = reg.ReplaceAllString(fact.Verb, replace) fact.Verb = reg.ReplaceAllString(fact.Verb, replace)
fact.Tidbit = reg.ReplaceAllString(fact.Tidbit, replace) fact.Tidbit = reg.ReplaceAllString(fact.Tidbit, replace)
fact.Count += 1 fact.Count += 1
fact.accessed = time.Now() fact.Accessed = time.Now()
fact.save(p.db) fact.Save(p.db)
} }
} else if len(parts) == 3 { } else if len(parts) == 3 {
// search for a factoid and print it // search for a factoid and print it
result, err := getFacts(p.db, trigger, parts[1]) result, err := getFacts(p.db, trigger, parts[1])
if err != nil { if err != nil {
log.Println("Error getting facts: ", trigger, err) log.Error().
p.Bot.SendMessage(message.Channel, "bzzzt") Err(err).
Str("trigger", trigger).
Msg("Error getting facts")
p.Bot.Send(c, bot.Message, message.Channel, "bzzzt")
return true return true
} }
count := len(result) count := len(result)
if count == 0 { 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 return true
} }
if parts[2] == "g" && len(result) > 4 { if parts[2] == "g" && len(result) > 4 {
// summarize // summarize
result = result[:4] result = result[:4]
} else { } else {
p.sayFact(message, *result[0]) p.sayFact(c, message, *result[0])
return true return true
} }
msg := fmt.Sprintf("%s ", trigger) msg := fmt.Sprintf("%s ", trigger)
@ -589,9 +622,9 @@ func (p *Factoid) changeFact(message msg.Message) bool {
if count > 4 { if count > 4 {
msg = fmt.Sprintf("%s | ...and %d others", msg, count) 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 { } 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 return true
} }
@ -599,79 +632,77 @@ func (p *Factoid) changeFact(message msg.Message) bool {
// Message responds to the bot hook on recieving messages. // 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. // 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. // 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?" { 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 // This plugin has no business with normal messages
if !message.Command { if !message.Command {
// look for any triggers in the db matching this message // 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") { 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 ") m := strings.TrimPrefix(message.Body, "alias ")
parts := strings.SplitN(m, "->", 2) parts := strings.SplitN(m, "->", 2)
if len(parts) != 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 return true
} }
a := aliasFromStrings(strings.TrimSpace(parts[1]), strings.TrimSpace(parts[0])) a := aliasFromStrings(strings.TrimSpace(parts[1]), strings.TrimSpace(parts[0]))
if err := a.save(p.db); err != nil { 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 { } else {
p.Bot.SendAction(message.Channel, "learns a new synonym") p.Bot.Send(c, bot.Action, message.Channel, "learns a new synonym")
} }
return true return true
} }
if strings.ToLower(message.Body) == "factoid" { if strings.ToLower(message.Body) == "factoid" {
if fact := p.randomFact(); fact != nil { if fact := p.randomFact(); fact != nil {
p.sayFact(message, *fact) p.sayFact(c, message, *fact)
return true return true
} }
log.Println("Got a nil fact.") log.Debug().Msg("Got a nil fact.")
} }
if strings.ToLower(message.Body) == "forget that" { if strings.ToLower(message.Body) == "forget that" {
return p.forgetLastFact(message) return p.forgetLastFact(c, message)
} }
if changeOperator(message.Body) != "" { if changeOperator(message.Body) != "" {
return p.changeFact(message) return p.changeFact(c, message)
} }
action := findAction(message.Body) action := findAction(message.Body)
if action != "" { if action != "" {
return p.learnAction(message, action) return p.learnAction(c, message, action)
} }
// look for any triggers in the db matching this message // look for any triggers in the db matching this message
if p.trigger(message) { if p.trigger(c, message) {
return true return true
} }
// We didn't find anything, panic! // 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 return true
} }
// 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 *Factoid) Help(channel string, parts []string) { func (p *FactoidPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "I can learn facts and spit them back out. You can say \"this is that\" or \"he <has> $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 learn facts and spit them back out. You can say \"this is that\" or \"he <has> $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.") p.Bot.Send(c, bot.Message, message.Channel, "I can also figure out some variables including: $nonzero, $digit, $nick, and $someone.")
} return true
// 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
} }
// Pull a fact at random from the database // Pull a fact at random from the database
func (p *Factoid) randomFact() *factoid { func (p *FactoidPlugin) randomFact() *Factoid {
f, err := getSingle(p.db) f, err := GetSingle(p.db)
if err != nil { if err != nil {
fmt.Println("Error getting a fact: ", err) fmt.Println("Error getting a fact: ", err)
return nil return nil
@ -680,8 +711,13 @@ func (p *Factoid) 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 *Factoid) factTimer(channel string) { func (p *FactoidPlugin) factTimer(c bot.Connector, channel string) {
duration := time.Duration(p.Bot.Config().Factoid.QuoteTime) * time.Minute 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() myLastMsg := time.Now()
for { for {
time.Sleep(time.Duration(5) * time.Second) // why 5? time.Sleep(time.Duration(5) * time.Second) // why 5?
@ -695,12 +731,17 @@ func (p *Factoid) 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().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 { if success && tdelta > duration && earlier {
fact := p.randomFact() fact := p.randomFact()
if fact == nil { 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 continue
} }
@ -711,23 +752,18 @@ func (p *Factoid) factTimer(channel string) {
User: &users[rand.Intn(len(users))], User: &users[rand.Intn(len(users))],
Channel: channel, Channel: channel,
} }
p.sayFact(message, *fact) p.sayFact(c, message, *fact)
myLastMsg = time.Now() myLastMsg = time.Now()
} }
} }
} }
// Handler for bot's own messages
func (p *Factoid) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired // 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/req", p.serveQuery)
http.HandleFunc("/factoid", p.serveQuery) http.HandleFunc("/factoid", p.serveQuery)
tmp := "/factoid" p.Bot.RegisterWeb("/factoid", "Factoid")
return &tmp
} }
func linkify(text string) template.HTML { func linkify(text string) template.HTML {
@ -739,30 +775,40 @@ func linkify(text string) template.HTML {
} }
return template.HTML(strings.Join(parts, " ")) 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) { entries, err := getFacts(p.db, info.Query, "")
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)
if err != nil { 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 { 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()})
}

View File

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

View File

@ -7,106 +7,109 @@ package fact
// 2016-01-15 Later note, why are these in plugins and the server is in bot? // 2016-01-15 Later note, why are these in plugins and the server is in bot?
var factoidIndex string = ` var factoidIndex = `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<title>Factoids</title> <!-- Load required Bootstrap and BootstrapVue CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.0/base-min.css"> <link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- DataTables CSS --> <!-- Load polyfills to support older browsers -->
<link rel="stylesheet" type="text/css" href="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css"> <script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CMutationObserver"></script>
<!-- jQuery -->
<script type="text/javascript" charset="utf8" src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js"></script>
<!-- DataTables -->
<script type="text/javascript" charset="utf8" src="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<meta charset="UTF-8">
<title>Factoids</title>
</head> </head>
<div> <body>
<form action="/factoid" method="GET" class="pure-form">
<fieldset>
<legend>Search for a factoid</legend>
<input type="text" name="entry" placeholder="trigger" value="{{.Search}}" />
<button type="submit" class="pure-button notice">Find</button>
</fieldset>
</form>
</div>
<div> <div id="app">
<style scoped> <b-navbar>
<b-navbar-brand>Factoids</b-navbar-brand>
.pure-button-success, <b-navbar-nav>
.pure-button-error, <b-nav-item v-for="item in nav" :href="item.URL" :active="item.Name === 'Factoid'">{{ "{{ item.Name }}" }}</b-nav-item>
.pure-button-warning, </b-navbar-nav>
.pure-button-secondary { </b-navbar>
color: white; <b-alert
border-radius: 4px; dismissable
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); variant="error"
padding: 2px; v-if="err"
} @dismissed="err = ''">
{{ "{{ err }}" }}
.pure-button-success { </b-alert>
background: rgb(76, 201, 71); /* this is a green */ <b-form @submit="runQuery">
} <b-container>
<b-row>
.pure-button-error { <b-col cols="10">
background: rgb(202, 60, 60); /* this is a maroon */ <b-input v-model="query"></b-input>
} </b-col>
<b-col cols="2">
.pure-button-warning { <b-button>Search</b-button>
background: orange; </b-col>
} </b-row>
<b-row>
.pure-button-secondary { <b-col>
background: rgb(95, 198, 218); /* this is a light blue */ <b-table
} fixed
:items="results"
</style> :fields="fields"></b-table>
</b-col>
{{if .Error}} </b-row>
<span id="error" class="pure-button-error">{{.Error}}</span> </b-container>
{{end}} </b-form>
</div>
{{if .Count}}
<span id="count" class="pure-button-success">Found {{.Count}} entries.</span>
{{end}}
</div>
{{if .Entries}}
<div style="padding-top: 1em;">
<table class="pure-table" id="factTable">
<thead>
<tr>
<th>Trigger</th>
<th>Full Text</th>
<th>Author</th>
<th># Hits</th>
</tr>
</thead>
<tbody>
{{range .Entries}}
<tr>
<td>{{linkify .Fact}}</td>
<td>{{linkify .Tidbit}}</td>
<td>{{linkify .Owner}}</td>
<td>{{.Count}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<script>
$(document).ready(function(){
$('#factTable').dataTable({
"bPaginate": false
});
});
</script>
<script>
var router = new VueRouter({
mode: 'history',
routes: []
});
var app = new Vue({
el: '#app',
router,
data: {
err: '',
nav: {{ .Nav }},
query: '',
results: [],
fields: [
{ key: 'Fact', sortable: true },
{ key: 'Tidbit', sortable: true },
{ key: 'Owner', sortable: true },
{ key: 'Count' }
]
},
mounted() {
if (this.$route.query.query) {
this.query = this.$route.query.query;
this.runQuery()
}
},
computed: {
result0: function() {
return this.results[0] || "";
}
},
methods: {
runQuery: function(evt) {
if (evt) {
evt.preventDefault();
evt.stopPropagation()
}
axios.post('/factoid/api', {query: this.query})
.then(resp => {
this.results = resp.data;
})
.catch(err => (this.err = err));
}
}
})
</script>
</body>
</html> </html>
` `

View File

@ -5,12 +5,12 @@ package first
import ( import (
"database/sql" "database/sql"
"fmt" "fmt"
"log"
"regexp" "regexp"
"strings" "strings"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "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. // This is a first plugin to serve as an example and quick copy/paste for new plugins.
type FirstPlugin struct { type FirstPlugin struct {
First *FirstEntry Bot bot.Bot
Bot bot.Bot db *sqlx.DB
db *sqlx.DB
} }
type FirstEntry struct { type FirstEntry struct {
id int64 id int64
day time.Time day time.Time
time time.Time time time.Time
body string channel string
nick string body string
saved bool nick string
saved bool
} }
// Insert or update the first entry // Insert or update the first entry
func (fe *FirstEntry) save(db *sqlx.DB) error { func (fe *FirstEntry) save(db *sqlx.DB) error {
if _, err := db.Exec(`insert into first (day, time, body, nick) if _, err := db.Exec(`insert into first (day, time, channel, body, nick)
values (?, ?, ?, ?)`, values (?, ?, ?, ?, ?)`,
fe.day.Unix(), fe.day.Unix(),
fe.time.Unix(), fe.time.Unix(),
fe.channel,
fe.body, fe.body,
fe.nick, fe.nick,
); err != nil { ); err != nil {
@ -48,34 +49,33 @@ 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 New(b bot.Bot) *FirstPlugin { 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, id integer primary key,
day integer, day integer,
time integer, time integer,
channel string,
body string, body string,
nick 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 { if err != nil {
log.Fatal("Could not initialize first plugin: ", err) log.Fatal().
Err(err).
Msg("Could not create first table")
} }
return &FirstPlugin{ log.Info().Msgf("First plugin initialized with day: %s",
Bot: b, midnight(time.Now()))
db: b.DB(),
First: first, 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 // Get last first entry
var id sql.NullInt64 var id sql.NullInt64
var day sql.NullInt64 var day sql.NullInt64
@ -85,8 +85,9 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
err := db.QueryRow(`select err := db.QueryRow(`select
id, max(day), time, body, nick from first id, max(day), time, body, nick from first
where channel = ?
limit 1; limit 1;
`).Scan( `, channel).Scan(
&id, &id,
&day, &day,
&timeEntered, &timeEntered,
@ -95,20 +96,22 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
) )
switch { switch {
case err == sql.ErrNoRows || !id.Valid: case err == sql.ErrNoRows || !id.Valid:
log.Println("No previous first entries") log.Info().Msg("No previous first entries")
return nil, nil return nil, nil
case err != 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 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{ return &FirstEntry{
id: id.Int64, id: id.Int64,
day: time.Unix(day.Int64, 0), day: time.Unix(day.Int64, 0),
time: time.Unix(timeEntered.Int64, 0), time: time.Unix(timeEntered.Int64, 0),
body: body.String, channel: channel,
nick: nick.String, body: body.String,
saved: true, nick: nick.String,
saved: true,
}, nil }, nil
} }
@ -117,7 +120,11 @@ func midnight(t time.Time) time.Time {
return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) 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) t0 := midnight(t)
return t0.Before(midnight(time.Now())) 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. // 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. // 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. // Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *FirstPlugin) Message(message msg.Message) bool { func (p *FirstPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
// This bot does not reply to anything log.Debug().
Interface("msg", message).
Msg("First is looking at a message")
if p.First == nil && p.allowed(message) { if message.IsIM {
log.Printf("No previous first. Recording new first: %s", message.Body) log.Debug().Msg("Skipping IM")
p.recordFirst(message)
return false 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) m := strings.ToLower(message.Body)
if r.Replace(msg) == "whos on first" { if r.Replace(m) == "whos on first" && first != nil {
p.announceFirst(message) p.announceFirst(c, first)
return true return true
} }
@ -152,81 +175,70 @@ func (p *FirstPlugin) Message(message msg.Message) bool {
} }
func (p *FirstPlugin) allowed(message msg.Message) bool { func (p *FirstPlugin) allowed(message msg.Message) bool {
for _, msg := range p.Bot.Config().Bad.Msgs { for _, m := range p.Bot.Config().GetArray("Bad.Msgs", []string{}) {
match, err := regexp.MatchString(msg, strings.ToLower(message.Body)) match, err := regexp.MatchString(m, strings.ToLower(message.Body))
if err != nil { if err != nil {
log.Println("Bad regexp: ", err) log.Error().Err(err).Msg("Bad regexp")
} }
if match { 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 return false
} }
} }
for _, host := range p.Bot.Config().Bad.Hosts { for _, host := range p.Bot.Config().GetArray("Bad.Hosts", []string{}) {
if host == message.Host { 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 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 { 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 false
} }
} }
return true return true
} }
func (p *FirstPlugin) recordFirst(message msg.Message) { func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) {
log.Println("Recording first: ", message.User.Name, ":", message.Body) log.Info().
p.First = &FirstEntry{ Str("channel", message.Channel).
day: midnight(time.Now()), Str("user", message.User.Name).
time: message.Time, Str("body", message.Body).
body: message.Body, Msg("Recording first")
nick: message.User.Name, 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) log.Info().Msgf("recordFirst: %+v", first.day)
err := p.First.save(p.db) err := first.save(p.db)
if err != nil { if err != nil {
log.Println("Error saving first entry: ", err) log.Error().Err(err).Msg("Error saving first entry")
return return
} }
p.announceFirst(message) p.announceFirst(c, first)
} }
func (p *FirstPlugin) announceFirst(message msg.Message) { func (p *FirstPlugin) announceFirst(c bot.Connector, first *FirstEntry) {
c := message.Channel ch := first.channel
if p.First != nil { p.Bot.Send(c, bot.Message, ch, fmt.Sprintf("%s had first at %s with the message: \"%s\"",
p.Bot.SendMessage(c, fmt.Sprintf("%s had first at %s with the message: \"%s\"", first.nick, first.time.Format("15:04"), first.body))
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
} }
// 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 *FirstPlugin) Help(channel string, parts []string) { func (p *FirstPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Sorry, First does not do a goddamn thing.") 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 }

View File

@ -6,10 +6,11 @@ package inventory
import ( import (
"fmt" "fmt"
"log"
"regexp" "regexp"
"strings" "strings"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
@ -24,38 +25,41 @@ type InventoryPlugin struct {
} }
// New creates a new InventoryPlugin with the Plugin interface // New creates a new InventoryPlugin with the Plugin interface
func New(bot bot.Bot) *InventoryPlugin { func New(b bot.Bot) *InventoryPlugin {
config := bot.Config() config := b.Config()
nick := config.Get("nick", "bot")
r1, err := regexp.Compile("take this (.+)") r1, err := regexp.Compile("take this (.+)")
checkerr(err) checkerr(err)
r2, err := regexp.Compile("have a (.+)") r2, err := regexp.Compile("have a (.+)")
checkerr(err) 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) checkerr(err)
r4, err := regexp.Compile(fmt.Sprintf("gives %s (.+)", config.Nick)) r4, err := regexp.Compile(fmt.Sprintf("gives %s (.+)", nick))
checkerr(err) 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) checkerr(err)
p := InventoryPlugin{ p := &InventoryPlugin{
DB: bot.DB(), DB: b.DB(),
bot: bot, bot: b,
config: config, config: config,
r1: r1, r2: r2, r3: r3, r4: r4, r5: r5, r1: r1, r2: r2, r3: r3, r4: r4, r5: r5,
} }
bot.RegisterFilter("$item", p.itemFilter) b.RegisterFilter("$item", p.itemFilter)
bot.RegisterFilter("$giveitem", p.giveItemFilter) b.RegisterFilter("$giveitem", p.giveItemFilter)
_, err = p.DB.Exec(`create table if not exists inventory ( _, err = p.DB.Exec(`create table if not exists inventory (
item string primary key item string primary key
);`) );`)
if err != nil { 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 { func (p *InventoryPlugin) giveItemFilter(input string) string {
@ -74,49 +78,49 @@ func (p *InventoryPlugin) itemFilter(input string) string {
return input 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 m := message.Body
log.Printf("inventory trying to read %+v", message) log.Debug().Msgf("inventory trying to read %+v", message)
if message.Command { if message.Command {
if strings.ToLower(m) == "inventory" { if strings.ToLower(m) == "inventory" {
items := p.getAll() items := p.getAll()
say := "I'm not holding anything" say := "I'm not holding anything"
if len(items) > 0 { 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, ", ")) 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 return true
} }
// <Randall> Bucket[:,] take this (.+) // <Randall> Bucket[:,] take this (.+)
// <Randall> Bucket[:,] have a (.+) // <Randall> Bucket[:,] have a (.+)
if matches := p.r1.FindStringSubmatch(m); len(matches) > 0 { if matches := p.r1.FindStringSubmatch(m); len(matches) > 0 {
log.Printf("Found item to add: %s", matches[1]) log.Debug().Msgf("Found item to add: %s", matches[1])
return p.addItem(message, matches[1]) return p.addItem(c, message, matches[1])
} }
if matches := p.r2.FindStringSubmatch(m); len(matches) > 0 { if matches := p.r2.FindStringSubmatch(m); len(matches) > 0 {
log.Printf("Found item to add: %s", matches[1]) log.Debug().Msgf("Found item to add: %s", matches[1])
return p.addItem(message, matches[1]) return p.addItem(c, message, matches[1])
} }
} }
if message.Action { if message.Action {
log.Println("Inventory found an action") log.Debug().Msg("Inventory found an action")
// * Randall puts (.+) in Bucket([^a-zA-Z].*)? // * Randall puts (.+) in Bucket([^a-zA-Z].*)?
// * Randall gives Bucket (.+) // * Randall gives Bucket (.+)
// * Randall gives (.+) to Bucket([^a-zA-Z].*)? // * Randall gives (.+) to Bucket([^a-zA-Z].*)?
if matches := p.r3.FindStringSubmatch(m); len(matches) > 0 { if matches := p.r3.FindStringSubmatch(m); len(matches) > 0 {
log.Printf("Found item to add: %s", matches[1]) log.Debug().Msgf("Found item to add: %s", matches[1])
return p.addItem(message, matches[1]) return p.addItem(c, message, matches[1])
} }
if matches := p.r4.FindStringSubmatch(m); len(matches) > 0 { if matches := p.r4.FindStringSubmatch(m); len(matches) > 0 {
log.Printf("Found item to add: %s", matches[1]) log.Debug().Msgf("Found item to add: %s", matches[1])
return p.addItem(message, matches[1]) return p.addItem(c, message, matches[1])
} }
if matches := p.r5.FindStringSubmatch(m); len(matches) > 0 { if matches := p.r5.FindStringSubmatch(m); len(matches) > 0 {
log.Printf("Found item to add: %s", matches[1]) log.Debug().Msgf("Found item to add: %s", matches[1])
return p.addItem(message, matches[1]) return p.addItem(c, message, matches[1])
} }
} }
return false return false
@ -128,12 +132,12 @@ func (p *InventoryPlugin) removeRandom() string {
&name, &name,
) )
if err != nil { if err != nil {
log.Printf("Error finding random entry: %s", err) log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR" return "IAMERROR"
} }
_, err = p.Exec(`delete from inventory where item=?`, name) _, err = p.Exec(`delete from inventory where item=?`, name)
if err != nil { if err != nil {
log.Printf("Error finding random entry: %s", err) log.Error().Err(err).Msgf("Error finding random entry")
return "IAMERROR" return "IAMERROR"
} }
return name return name
@ -143,7 +147,7 @@ func (p *InventoryPlugin) count() int {
var output int var output int
err := p.QueryRow(`select count(*) as count from inventory`).Scan(&output) err := p.QueryRow(`select count(*) as count from inventory`).Scan(&output)
if err != nil { if err != nil {
log.Printf("Error checking for item: %s", err) log.Error().Err(err).Msg("Error checking for item")
return -1 return -1
} }
return output return output
@ -155,7 +159,7 @@ func (p *InventoryPlugin) random() string {
&name, &name,
) )
if err != nil { if err != nil {
log.Printf("Error finding random entry: %s", err) log.Error().Err(err).Msg("Error finding random entry")
return "IAMERROR" return "IAMERROR"
} }
return name return name
@ -164,7 +168,7 @@ func (p *InventoryPlugin) random() string {
func (p *InventoryPlugin) getAll() []string { func (p *InventoryPlugin) getAll() []string {
rows, err := p.Queryx(`select item from inventory`) rows, err := p.Queryx(`select item from inventory`)
if err != nil { if err != nil {
log.Printf("Error getting all items: %s", err) log.Error().Err(err).Msg("Error getting all items")
return []string{} return []string{}
} }
output := []string{} output := []string{}
@ -181,7 +185,7 @@ func (p *InventoryPlugin) exists(i string) bool {
var output int var output int
err := p.QueryRow(`select count(*) as count from inventory where item=?`, i).Scan(&output) err := p.QueryRow(`select count(*) as count from inventory where item=?`, i).Scan(&output)
if err != nil { if err != nil {
log.Printf("Error checking for item: %s", err) log.Error().Err(err).Msg("Error checking for item")
return false return false
} }
return output > 0 return output > 0
@ -190,51 +194,34 @@ func (p *InventoryPlugin) exists(i string) bool {
func (p *InventoryPlugin) remove(i string) { func (p *InventoryPlugin) remove(i string) {
_, err := p.Exec(`delete from inventory where item=?`, i) _, err := p.Exec(`delete from inventory where item=?`, i)
if err != nil { 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) { 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 return true
} }
var removed string var removed string
if p.count() > p.config.Inventory.Max { max := p.config.GetInt("inventory.max", 10)
if p.count() > max {
removed = p.removeRandom() removed = p.removeRandom()
} }
_, err := p.Exec(`INSERT INTO inventory (item) values (?)`, i) _, err := p.Exec(`INSERT INTO inventory (item) values (?)`, i)
if err != nil { if err != nil {
log.Printf("Error inserting new inventory item: %s", err) log.Error().Err(err).Msg("Error inserting new inventory item")
} }
if removed != "" { 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 { } 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 return true
} }
func checkerr(e error) { func checkerr(e error) {
if e != nil { 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 }

View File

@ -20,19 +20,20 @@ type LeftpadPlugin struct {
} }
// 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(b bot.Bot) *LeftpadPlugin {
p := LeftpadPlugin{ p := &LeftpadPlugin{
bot: bot, bot: b,
config: bot.Config(), config: b.Config(),
} }
return &p b.Register(p, bot.Message, p.message)
return p
} }
type leftpadResp struct { type leftpadResp struct {
Str string 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 { if !message.Command {
return false return false
} }
@ -42,39 +43,22 @@ func (p *LeftpadPlugin) Message(message msg.Message) bool {
padchar := parts[1] padchar := parts[1]
length, err := strconv.Atoi(parts[2]) length, err := strconv.Atoi(parts[2])
if err != nil { if err != nil {
p.bot.SendMessage(message.Channel, "Invalid padding number") p.bot.Send(c, bot.Message, message.Channel, "Invalid padding number")
return true return true
} }
if length > p.config.LeftPad.MaxLen && p.config.LeftPad.MaxLen > 0 { maxLen, who := p.config.GetInt("LeftPad.MaxLen", 50), p.config.Get("LeftPad.Who", "Putin")
msg := fmt.Sprintf("%s would kill me if I did that.", p.config.LeftPad.Who) if length > maxLen && maxLen > 0 {
p.bot.SendMessage(message.Channel, msg) msg := fmt.Sprintf("%s would kill me if I did that.", who)
p.bot.Send(c, bot.Message, message.Channel, msg)
return true return true
} }
text := strings.Join(parts[3:], " ") text := strings.Join(parts[3:], " ")
res := leftpad.LeftPad(text, length, padchar) res := leftpad.LeftPad(text, length, padchar)
p.bot.SendMessage(message.Channel, res) p.bot.Send(c, bot.Message, message.Channel, res)
return true return true
} }
return false 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 }

View File

@ -3,6 +3,7 @@
package leftpad package leftpad
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -13,12 +14,12 @@ import (
"github.com/velour/catbase/plugins/counter" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -31,75 +32,57 @@ func makePlugin(t *testing.T) (*LeftpadPlugin, *bot.MockBot) {
counter.New(mb) counter.New(mb)
p := New(mb) p := New(mb)
assert.NotNil(t, p) assert.NotNil(t, p)
p.config.Set("LeftPad.MaxLen", "0")
return p, mb return p, mb
} }
func TestLeftpad(t *testing.T) { func TestLeftpad(t *testing.T) {
p, mb := makePlugin(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.Contains(t, mb.Messages[0], "testtest")
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
} }
func TestBadNumber(t *testing.T) { func TestBadNumber(t *testing.T) {
p, mb := makePlugin(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.Contains(t, mb.Messages[0], "Invalid")
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
} }
func TestNotCommand(t *testing.T) { func TestNotCommand(t *testing.T) {
p, mb := makePlugin(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) assert.Len(t, mb.Messages, 0)
} }
func TestNoMaxLen(t *testing.T) { func TestNoMaxLen(t *testing.T) {
p, mb := makePlugin(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.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "dicks") assert.Contains(t, mb.Messages[0], "dicks")
} }
func Test50Padding(t *testing.T) { func Test50Padding(t *testing.T) {
p, mb := makePlugin(t) p, mb := makePlugin(t)
p.config.LeftPad.MaxLen = 50 p.config.Set("LeftPad.MaxLen", "50")
p.Message(makeMessage("!leftpad dicks 100 dicks")) assert.Equal(t, 50, p.config.GetInt("LeftPad.MaxLen", 100))
p.message(makeMessage("!leftpad dicks 100 dicks"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "kill me") assert.Contains(t, mb.Messages[0], "kill me")
} }
func TestUnder50Padding(t *testing.T) { func TestUnder50Padding(t *testing.T) {
p, mb := makePlugin(t) p, mb := makePlugin(t)
p.config.LeftPad.MaxLen = 50 p.config.Set("LeftPad.MaxLen", "50")
p.Message(makeMessage("!leftpad dicks 49 dicks")) p.message(makeMessage("!leftpad dicks 49 dicks"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "dicks") assert.Contains(t, mb.Messages[0], "dicks")
} }
func TestNotPadding(t *testing.T) { func TestNotPadding(t *testing.T) {
p, mb := makePlugin(t) p, mb := makePlugin(t)
p.Message(makeMessage("!lololol")) p.message(makeMessage("!lololol"))
assert.Len(t, mb.Messages, 0) 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())
}

View File

@ -27,29 +27,32 @@ type NerdepediaPlugin struct {
} }
// NewNerdepediaPlugin creates a new NerdepediaPlugin with the Plugin interface // NewNerdepediaPlugin creates a new NerdepediaPlugin with the Plugin interface
func New(bot bot.Bot) *NerdepediaPlugin { func New(b bot.Bot) *NerdepediaPlugin {
return &NerdepediaPlugin{ np := &NerdepediaPlugin{
bot: bot, bot: b,
config: bot.Config(), 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. // 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. // 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. // 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) lowerCase := strings.ToLower(message.Body)
query := "" query := ""
if lowerCase == "may the force be with you" || lowerCase == "help me obi-wan" { if lowerCase == "may the force be with you" || lowerCase == "help me obi-wan" {
query = "http://starwars.wikia.com/wiki/Special:Random" query = "http://starwars.wikia.com/wiki/Special:Random"
} else if lowerCase == "beam me up scotty" || lowerCase == "live long and prosper" { } else if lowerCase == "beam me up scotty" || lowerCase == "live long and prosper" {
query = "http://memory-alpha.wikia.com/wiki/Special:Random" 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" 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" { } 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" query = "http://lotr.wikia.com/wiki/Special:Random"
} else if lowerCase == "gotta catch em all" { } else if lowerCase == "pikachu i choose you" || lowerCase == "gotta catch em all" {
query = "https://bulbapedia.bulbagarden.net/wiki/Special:Random" query = "http://pokemon.wikia.com/wiki/Special:Random"
} }
if query != "" { if query != "" {
@ -78,7 +81,7 @@ func (p *NerdepediaPlugin) Message(message msg.Message) bool {
} }
if description != "" && link != "" { 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 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *NerdepediaPlugin) Help(channel string, parts []string) { func (p *NerdepediaPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.SendMessage(channel, "nerd stuff") 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 }

View File

@ -3,6 +3,7 @@
package nerdepedia package nerdepedia
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, 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() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
} }

115
plugins/newsbid/newsbid.go Normal file
View File

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

View File

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

View File

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

View File

@ -16,34 +16,37 @@ import (
) )
type PickerPlugin struct { type PickerPlugin struct {
Bot bot.Bot bot bot.Bot
} }
// NewPickerPlugin creates a new PickerPlugin with the Plugin interface // NewPickerPlugin creates a new PickerPlugin with the Plugin interface
func New(bot bot.Bot) *PickerPlugin { func New(b bot.Bot) *PickerPlugin {
return &PickerPlugin{ pp := &PickerPlugin{
Bot: bot, 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. // 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. // 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. // 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") { if !strings.HasPrefix(message.Body, "pick") {
return false return false
} }
n, items, err := p.parse(message.Body) n, items, err := p.parse(message.Body)
if err != nil { if err != nil {
p.Bot.SendMessage(message.Channel, err.Error()) p.bot.Send(c, bot.Message, message.Channel, err.Error())
return true return true
} }
if n == 1 { if n == 1 {
item := items[rand.Intn(len(items))] item := items[rand.Intn(len(items))]
out := fmt.Sprintf("I've chosen %q for you.", strings.TrimSpace(item)) 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 return true
} }
@ -59,7 +62,7 @@ func (p *PickerPlugin) Message(message msg.Message) bool {
fmt.Fprintf(&b, ", %q", item) fmt.Fprintf(&b, ", %q", item)
} }
b.WriteString(" }") b.WriteString(" }")
p.Bot.SendMessage(message.Channel, b.String()) p.bot.Send(c, bot.Message, message.Channel, b.String())
return true 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *PickerPlugin) Help(channel string, parts []string) { func (p *PickerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Choose from a list of options. Try \"pick {a,b,c}\".") 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 }

View File

@ -3,6 +3,7 @@
package picker package picker
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -29,7 +30,7 @@ func TestPick2(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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) assert.Len(t, mb.Messages, 1)
if !res { if !res {
t.Fatalf("expected a successful choice, got %q", mb.Messages[0]) t.Fatalf("expected a successful choice, got %q", mb.Messages[0])
@ -40,7 +41,7 @@ func TestPickDefault(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
_ = c.Message(makeMessage("!pick { a}")) _ = c.message(makeMessage("!pick { a}"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Equal(t, `I've chosen "a" for you.`, mb.Messages[0]) assert.Equal(t, `I've chosen "a" for you.`, mb.Messages[0])
} }

View File

@ -1,15 +1,3 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors. // © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package plugins 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()
}

View File

@ -11,36 +11,38 @@ import (
) )
type ReactionPlugin struct { type ReactionPlugin struct {
Bot bot.Bot bot bot.Bot
Config *config.Config config *config.Config
} }
func New(bot bot.Bot) *ReactionPlugin { func New(b bot.Bot) *ReactionPlugin {
return &ReactionPlugin{ rp := &ReactionPlugin{
Bot: bot, bot: b,
Config: bot.Config(), 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 harrass := false
for _, nick := range p.Config.Reaction.HarrassList { for _, nick := range p.config.GetArray("Reaction.HarrassList", []string{}) {
if message.User.Name == nick { if message.User.Name == nick {
harrass = true harrass = true
break break
} }
} }
chance := p.Config.Reaction.GeneralChance chance := p.config.GetFloat64("Reaction.GeneralChance", 0.01)
negativeWeight := 1 negativeWeight := 1
if harrass { if harrass {
chance = p.Config.Reaction.HarrassChance chance = p.config.GetFloat64("Reaction.HarrassChance", 0.05)
negativeWeight = p.Config.Reaction.NegativeHarrassmentMultiplier negativeWeight = p.config.GetInt("Reaction.NegativeHarrassmentMultiplier", 2)
} }
if rand.Float64() < chance { if rand.Float64() < chance {
numPositiveReactions := len(p.Config.Reaction.PositiveReactions) numPositiveReactions := len(p.config.GetArray("Reaction.PositiveReactions", []string{}))
numNegativeReactions := len(p.Config.Reaction.NegativeReactions) numNegativeReactions := len(p.config.GetArray("Reaction.NegativeReactions", []string{}))
maxIndex := numPositiveReactions + numNegativeReactions*negativeWeight maxIndex := numPositiveReactions + numNegativeReactions*negativeWeight
@ -49,33 +51,15 @@ func (p *ReactionPlugin) Message(message msg.Message) bool {
reaction := "" reaction := ""
if index < numPositiveReactions { if index < numPositiveReactions {
reaction = p.Config.Reaction.PositiveReactions[index] reaction = p.config.GetArray("Reaction.PositiveReactions", []string{})[index]
} else { } else {
index -= numPositiveReactions index -= numPositiveReactions
index %= numNegativeReactions 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 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 }

View File

@ -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 <nick> <snippet> 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)
}

View File

@ -1,6 +1,7 @@
package fact package remember
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -8,6 +9,7 @@ import (
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "github.com/velour/catbase/bot/user"
"github.com/velour/catbase/plugins/fact"
) )
func makeMessage(nick, payload string) msg.Message { 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() mb := bot.NewMockBot()
f := New(mb) // for DB table f := fact.New(mb) // for DB table
p := NewRemember(mb) p := New(mb)
assert.NotNil(t, p) assert.NotNil(t, p)
return p, f, mb return p, f, mb
} }
@ -42,11 +44,11 @@ func TestCornerCaseBug(t *testing.T) {
p, _, mb := makePlugin(t) p, _, mb := makePlugin(t)
for _, m := range msgs { for _, m := range msgs {
p.Message(m) p.message(&cli.CliPlugin{}, bot.Message, m)
} }
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "horse dick") 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.Nil(t, err)
assert.Contains(t, q.Tidbit, "horse dick") assert.Contains(t, q.Tidbit, "horse dick")
} }

View File

@ -5,13 +5,18 @@ package reminder
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/jmoiron/sqlx" "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"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "github.com/velour/catbase/config"
@ -22,11 +27,12 @@ const (
) )
type ReminderPlugin struct { type ReminderPlugin struct {
Bot bot.Bot bot bot.Bot
db *sqlx.DB db *sqlx.DB
mutex *sync.Mutex mutex *sync.Mutex
timer *time.Timer timer *time.Timer
config *config.Config config *config.Config
when *when.Parser
} }
type Reminder struct { type Reminder struct {
@ -38,10 +44,8 @@ type Reminder struct {
channel string channel string
} }
func New(bot bot.Bot) *ReminderPlugin { func New(b bot.Bot) *ReminderPlugin {
log.SetFlags(log.LstdFlags | log.Lshortfile) if _, err := b.DB().Exec(`create table if not exists reminders (
if bot.DBVersion() == 1 {
if _, err := bot.DB().Exec(`create table if not exists reminders (
id integer primary key, id integer primary key,
fromWho string, fromWho string,
toWho string, toWho string,
@ -49,33 +53,51 @@ func New(bot bot.Bot) *ReminderPlugin {
remindWhen string, remindWhen string,
channel string channel string
);`); err != nil { );`); err != nil {
log.Fatal(err) log.Fatal().Err(err)
}
} }
dur, _ := time.ParseDuration("1h") dur, _ := time.ParseDuration("1h")
timer := time.NewTimer(dur) timer := time.NewTimer(dur)
timer.Stop() timer.Stop()
w := when.New(nil)
w.Add(en.All...)
w.Add(common.All...)
plugin := &ReminderPlugin{ plugin := &ReminderPlugin{
Bot: bot, bot: b,
db: bot.DB(), db: b.DB(),
mutex: &sync.Mutex{}, mutex: &sync.Mutex{},
timer: timer, timer: timer,
config: bot.Config(), config: b.Config(),
when: w,
} }
plugin.queueUpNextReminder() 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 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 channel := message.Channel
from := message.User.Name 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) parts := strings.Fields(message.Body)
if len(parts) >= 5 { if len(parts) >= 5 {
@ -85,17 +107,16 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
who = from who = from
} }
dur, err := time.ParseDuration(parts[3]) dur, err = time.ParseDuration(parts[3])
if err != nil { 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 return true
} }
operator := strings.ToLower(parts[2]) operator := strings.ToLower(parts[2])
doConfirm := true doConfirm := true
if operator == "in" { if operator == "in" || operator == "at" || operator == "on" {
//one off reminder //one off reminder
//remind who in dur blah //remind who in dur blah
when := time.Now().UTC().Add(dur) 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" { } else if operator == "every" && strings.ToLower(parts[4]) == "for" {
//batch add, especially for reminding msherms to buy a kit //batch add, especially for reminding msherms to buy a kit
//remind who every dur for dur2 blah //remind who every dur for dur2 blah
dur2, err := time.ParseDuration(parts[5]) dur2, err = time.ParseDuration(parts[5])
if err != nil { 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 return true
} }
@ -123,9 +145,10 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
endTime := time.Now().UTC().Add(dur2) endTime := time.Now().UTC().Add(dur2)
what := strings.Join(parts[6:], " ") what := strings.Join(parts[6:], " ")
max := p.config.GetInt("Reminder.MaxBatchAdd", 10)
for i := 0; when.Before(endTime); i++ { for i := 0; when.Before(endTime); i++ {
if i >= p.config.Reminder.MaxBatchAdd { if i >= max {
p.Bot.SendMessage(channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.") p.bot.Send(c, bot.Message, channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.")
doConfirm = false doConfirm = false
break break
} }
@ -142,14 +165,14 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
when = when.Add(dur) when = when.Add(dur)
} }
} else { } 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 return true
} }
if doConfirm && from == who { 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 { } 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() p.queueUpNextReminder()
@ -169,22 +192,22 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
} }
} }
if err != nil { if err != nil {
p.Bot.SendMessage(channel, "listing failed.") p.bot.Send(c, bot.Message, channel, "listing failed.")
} else { } else {
p.Bot.SendMessage(channel, response) p.bot.Send(c, bot.Message, channel, response)
} }
return true return true
} else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" { } else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" {
id, err := strconv.ParseInt(parts[2], 10, 64) id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil { 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 { } else {
err := p.deleteReminder(id) err := p.deleteReminder(id)
if err == nil { 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 { } 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 return true
@ -193,20 +216,9 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
return false return false
} }
func (p *ReminderPlugin) Help(channel string, parts []string) { func (p *ReminderPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Pester someone with a reminder. Try \"remind <user> in <duration> message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration") p.bot.Send(c, bot.Message, message.Channel, "Pester someone with a reminder. Try \"remind <user> in <duration> message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration")
} return true
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) getNextReminder() *Reminder { func (p *ReminderPlugin) getNextReminder() *Reminder {
@ -214,7 +226,7 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
defer p.mutex.Unlock() defer p.mutex.Unlock()
rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;") rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;")
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil return nil
} }
defer rows.Close() defer rows.Close()
@ -223,19 +235,19 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
var reminder *Reminder var reminder *Reminder
for rows.Next() { for rows.Next() {
if once { if once {
log.Print("somehow got multiple rows") log.Debug().Msg("somehow got multiple rows")
} }
reminder = &Reminder{} reminder = &Reminder{}
var when string var when string
err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when, &reminder.channel) err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when, &reminder.channel)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil return nil
} }
reminder.when, err = time.Parse(TIMESTAMP, when) reminder.when, err = time.Parse(TIMESTAMP, when)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return nil 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) reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
} }
return err return err
} }
@ -262,7 +274,7 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
defer p.mutex.Unlock() defer p.mutex.Unlock()
res, err := p.db.Exec(`delete from reminders where id = ?;`, id) res, err := p.db.Exec(`delete from reminders where id = ?;`, id)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
} else { } else {
if affected, err := res.RowsAffected(); err != nil { if affected, err := res.RowsAffected(); err != nil {
return err return err
@ -273,12 +285,28 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
return err 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() p.mutex.Lock()
defer p.mutex.Unlock() 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) rows, err := p.db.Query(queryString)
if err != nil { if err != nil {
log.Print(err) log.Error().Err(err)
return "", nil return "", nil
} }
defer rows.Close() 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) reminders += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, when, reminder.id)
counter++ 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 return reminders, nil
} }
func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) { 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) { 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) { 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() { func (p *ReminderPlugin) queueUpNextReminder() {
@ -321,7 +351,7 @@ func (p *ReminderPlugin) queueUpNextReminder() {
} }
} }
func reminderer(p *ReminderPlugin) { func reminderer(c bot.Connector, p *ReminderPlugin) {
for { for {
<-p.timer.C <-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) 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 { if err := p.deleteReminder(reminder.id); err != nil {
log.Print(reminder.id) log.Error().
log.Print(err) Int64("id", reminder.id).
log.Fatal("this will cause problems, we need to stop now.") Err(err).
Msg("this will cause problems, we need to stop now.")
} }
} }
p.queueUpNextReminder() p.queueUpNextReminder()
} }
} }
func (p *ReminderPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -4,6 +4,7 @@ package reminder
import ( import (
"fmt" "fmt"
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -14,25 +15,16 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") return makeMessageBy(payload, "tester")
if isCmd {
payload = payload[1:]
}
return msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
Command: isCmd,
}
} }
func makeMessageBy(payload, by string) msg.Message { func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: by}, User: &user.User{Name: by},
Channel: "test", Channel: "test",
Body: payload, 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() mb := bot.NewMockBot()
c := New(mb) r := New(mb)
assert.NotNil(t, c) mb.DB().MustExec(`delete from reminders; delete from config;`)
res := c.Message(makeMessage("!remind me in 1s don't fail this test")) 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) time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
assert.True(t, res) assert.True(t, res)
@ -53,10 +50,8 @@ func TestMeReminder(t *testing.T) {
} }
func TestReminder(t *testing.T) { func TestReminder(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessage("!remind testuser in 1s don't fail this test"))
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 1s don't fail this test"))
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
assert.True(t, res) assert.True(t, res)
@ -65,12 +60,10 @@ func TestReminder(t *testing.T) {
} }
func TestReminderReorder(t *testing.T) { func TestReminderReorder(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessage("!remind testuser in 2s don't fail this test 2"))
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 2s don't fail this test 2"))
assert.True(t, res) 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) assert.True(t, res)
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
assert.Len(t, mb.Messages, 4) assert.Len(t, mb.Messages, 4)
@ -81,34 +74,28 @@ func TestReminderReorder(t *testing.T) {
} }
func TestReminderParse(t *testing.T) { func TestReminderParse(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessage("!remind testuser in unparseable don't fail this test"))
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in unparseable don't fail this test"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) 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) { func TestEmptyList(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessage("!list reminders"))
assert.NotNil(t, c)
res := c.Message(makeMessage("!list reminders"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "no pending reminders") assert.Contains(t, mb.Messages[0], "no pending reminders")
} }
func TestList(t *testing.T) { func TestList(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessage("!remind testuser in 5m don't fail this test 1"))
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 5m don't fail this test 1"))
assert.True(t, res) 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) assert.True(t, res)
res = c.Message(makeMessage("!list reminders")) res = c.message(makeMessage("!list reminders"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "1) tester -> testuser :: don't fail this test 1 @ ") 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) { func TestListBy(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
assert.True(t, res) 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) 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.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "don't fail this test 1 @ ") 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) { func TestListTo(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res) 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) 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.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.NotContains(t, mb.Messages[2], "don't fail this test 1 @ ") 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) { func TestToEmptyList(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res) 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) 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.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders") assert.Contains(t, mb.Messages[2], "no pending reminders")
} }
func TestFromEmptyList(t *testing.T) { func TestFromEmptyList(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res) 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) 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.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders") 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) { func TestBatchMax(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) c.config.Set("Reminder.MaxBatchAdd", "10")
c.config.Reminder.MaxBatchAdd = 10
assert.NotNil(t, c) 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) assert.True(t, res)
res = c.Message(makeMessage("!list reminders")) res = c.message(makeMessage("!list reminders"))
assert.True(t, res) assert.True(t, res)
time.Sleep(6 * time.Second) time.Sleep(6 * time.Second)
assert.Len(t, mb.Messages, 2) assert.Len(t, mb.Messages, 2)
@ -206,14 +170,13 @@ func TestBatchMax(t *testing.T) {
} }
func TestCancel(t *testing.T) { func TestCancel(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb)
assert.NotNil(t, c) 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) assert.True(t, res)
res = c.Message(makeMessage("!cancel reminder 1")) res = c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res) assert.True(t, res)
res = c.Message(makeMessage("!list reminders")) res = c.message(makeMessage("!list reminders"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 3) assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.") 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) { func TestCancelMiss(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!cancel reminder 1")) res := c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 1") assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 1")
} }
func TestHelp(t *testing.T) { func TestLimitList(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) c.config.Set("Reminder.MaxBatchAdd", "10")
c.config.Set("Reminder.MaxList", "25")
assert.NotNil(t, c) 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) 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

@ -20,7 +20,7 @@ const (
) )
type RPGPlugin struct { type RPGPlugin struct {
Bot bot.Bot bot bot.Bot
listenFor map[string]*board listenFor map[string]*board
} }
@ -98,45 +98,35 @@ func (b *board) checkAndMove(dx, dy int) int {
} }
func New(b bot.Bot) *RPGPlugin { func New(b bot.Bot) *RPGPlugin {
return &RPGPlugin{ rpg := &RPGPlugin{
Bot: b, bot: b,
listenFor: map[string]*board{}, 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" { if strings.ToLower(message.Body) == "start rpg" {
b := NewRandomBoard() 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.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 true
} }
return false 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) { func (p *RPGPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Go find a walkthrough or something.") identifier := args[0].(string)
} if strings.ToLower(message.User.Name) != strings.ToLower(p.bot.Config().Get("Nick", "bot")) {
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) {
if b, ok := p.listenFor[identifier]; ok { if b, ok := p.listenFor[identifier]; ok {
var res int var res int
@ -155,12 +145,12 @@ func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool {
switch res { switch res {
case OK: case OK:
p.Bot.Edit(message.Channel, b.toMessageString(), identifier) p.bot.Send(c, bot.Edit, message.Channel, b.toMessageString(), identifier)
case WIN: case WIN:
p.Bot.Edit(message.Channel, b.toMessageString(), identifier) p.bot.Send(c, 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.Reply, message.Channel, "congratulations, you beat the easiest level imaginable.", identifier)
case INVALID: 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 return true
} }

View File

@ -1,3 +1 @@
package rpgORdie package rpgORdie
import ()

View File

@ -12,7 +12,7 @@ import (
) )
type RSSPlugin struct { type RSSPlugin struct {
Bot bot.Bot bot bot.Bot
cache map[string]*cacheItem cache map[string]*cacheItem
shelfLife time.Duration shelfLife time.Duration
maxLines int maxLines int
@ -49,28 +49,31 @@ func (c *cacheItem) getCurrentPage(maxLines int) string {
return page return page
} }
func New(bot bot.Bot) *RSSPlugin { func New(b bot.Bot) *RSSPlugin {
return &RSSPlugin{ rss := &RSSPlugin{
Bot: bot, bot: b,
cache: map[string]*cacheItem{}, cache: map[string]*cacheItem{},
shelfLife: time.Minute * 20, shelfLife: time.Minute * time.Duration(b.Config().GetInt("rss.shelfLife", 20)),
maxLines: 5, 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) tokens := strings.Fields(message.Body)
numTokens := len(tokens) numTokens := len(tokens)
if numTokens == 2 && strings.ToLower(tokens[0]) == "rss" { if numTokens == 2 && strings.ToLower(tokens[0]) == "rss" {
if item, ok := p.cache[strings.ToLower(tokens[1])]; ok && time.Now().Before(item.expiration) { 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 return true
} else { } else {
fp := gofeed.NewParser() fp := gofeed.NewParser()
feed, err := fp.ParseURL(tokens[1]) feed, err := fp.ParseURL(tokens[1])
if err != nil { 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 return true
} }
item := &cacheItem{ item := &cacheItem{
@ -86,7 +89,7 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
p.cache[strings.ToLower(tokens[1])] = item 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 return true
} }
} }
@ -94,28 +97,8 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
return false 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. // Help responds to help requests. Every plugin must implement a help function.
func (p *RSSPlugin) Help(channel string, parts []string) { func (p *RSSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "try '!rss http://rss.cnn.com/rss/edition.rss'") 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 }

View File

@ -2,6 +2,7 @@ package rss
import ( import (
"fmt" "fmt"
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -11,12 +12,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -28,7 +29,7 @@ func TestRSS(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
} }
@ -38,7 +39,7 @@ func TestRSSPaging(t *testing.T) {
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
for i := 0; i < 20; i++ { 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) assert.True(t, res)
} }

View File

@ -2,12 +2,13 @@ package sisyphus
import ( import (
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -18,7 +19,7 @@ const (
) )
type SisyphusPlugin struct { type SisyphusPlugin struct {
Bot bot.Bot bot bot.Bot
listenFor map[string]*game listenFor map[string]*game
} }
@ -37,54 +38,54 @@ type game struct {
nextAns int 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 size := rand.Intn(9) + 2
g := game{ g := game{
channel: channel, channel: channel,
bot: bot, bot: b,
who: who, who: who,
start: time.Now(), start: time.Now(),
size: size, size: size,
current: size / 2, current: size / 2,
} }
g.id = bot.SendMessage(channel, g.toMessageString()) g.id, _ = b.Send(c, bot.Message, channel, g.toMessageString())
g.schedulePush() g.schedulePush(c)
g.scheduleDecrement() g.scheduleDecrement(c)
return &g return &g
} }
func (g *game) scheduleDecrement() { func (g *game) scheduleDecrement(c bot.Connector) {
if g.timers[0] != nil { if g.timers[0] != nil {
g.timers[0].Stop() g.timers[0].Stop()
} }
minDec := g.bot.Config().Sisyphus.MinDecrement minDec := g.bot.Config().GetInt("Sisyphus.MinDecrement", 10)
maxDec := g.bot.Config().Sisyphus.MinDecrement maxDec := g.bot.Config().GetInt("Sisyphus.MaxDecrement", 30)
g.nextDec = time.Now().Add(time.Duration((minDec + rand.Intn(maxDec))) * time.Minute) g.nextDec = time.Now().Add(time.Duration(minDec+rand.Intn(maxDec)) * time.Minute)
go func() { go func() {
t := time.NewTimer(g.nextDec.Sub(time.Now())) t := time.NewTimer(g.nextDec.Sub(time.Now()))
g.timers[0] = t g.timers[0] = t
select { select {
case <-t.C: case <-t.C:
g.handleDecrement() g.handleDecrement(c)
} }
}() }()
} }
func (g *game) schedulePush() { func (g *game) schedulePush(c bot.Connector) {
if g.timers[1] != nil { if g.timers[1] != nil {
g.timers[1].Stop() g.timers[1].Stop()
} }
minPush := g.bot.Config().Sisyphus.MinPush minPush := g.bot.Config().GetInt("Sisyphus.MinPush", 1)
maxPush := g.bot.Config().Sisyphus.MaxPush maxPush := g.bot.Config().GetInt("Sisyphus.MaxPush", 10)
g.nextPush = time.Now().Add(time.Duration(rand.Intn(maxPush)+minPush) * time.Minute) g.nextPush = time.Now().Add(time.Duration(rand.Intn(maxPush)+minPush) * time.Minute)
go func() { go func() {
t := time.NewTimer(g.nextPush.Sub(time.Now())) t := time.NewTimer(g.nextPush.Sub(time.Now()))
g.timers[1] = t g.timers[1] = t
select { select {
case <-t.C: case <-t.C:
g.handleNotify() g.handleNotify(c)
} }
}() }()
} }
@ -96,21 +97,21 @@ func (g *game) endGame() {
g.ended = true g.ended = true
} }
func (g *game) handleDecrement() { func (g *game) handleDecrement(c bot.Connector) {
g.current++ 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 { 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)) 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() g.endGame()
} else { } else {
g.scheduleDecrement() g.scheduleDecrement(c)
} }
} }
func (g *game) handleNotify() { func (g *game) handleNotify(c bot.Connector) {
g.bot.ReplyToMessageIdentifier(g.channel, "You can push now.\n"+g.generateQuestion(), g.id) g.bot.Send(c, bot.Reply, g.channel, "You can push now.\n"+g.generateQuestion(), g.id)
} }
func (g *game) generateQuestion() string { func (g *game) generateQuestion() string {
@ -162,43 +163,37 @@ func (g *game) toMessageString() string {
} }
func New(b bot.Bot) *SisyphusPlugin { func New(b bot.Bot) *SisyphusPlugin {
return &SisyphusPlugin{ sp := &SisyphusPlugin{
Bot: b, bot: b,
listenFor: map[string]*game{}, 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" { 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.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 true
} }
return false return false
} }
func (p *SisyphusPlugin) Help(channel string, parts []string) { func (p *SisyphusPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "https://en.wikipedia.org/wiki/Sisyphus") 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 { func (p *SisyphusPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
return false identifier := args[0].(string)
} if strings.ToLower(message.User.Name) != strings.ToLower(p.bot.Config().Get("Nick", "bot")) {
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) {
if g, ok := p.listenFor[identifier]; ok { 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 { if g.ended {
return false return false
@ -211,18 +206,18 @@ func (p *SisyphusPlugin) ReplyMessage(message msg.Message, identifier string) bo
if time.Now().After(g.nextPush) { if time.Now().After(g.nextPush) {
if g.checkAnswer(message.Body) { if g.checkAnswer(message.Body) {
p.Bot.Edit(message.Channel, g.toMessageString(), identifier) p.bot.Send(c, bot.Edit, message.Channel, g.toMessageString(), identifier)
g.schedulePush() g.schedulePush(c)
msg := fmt.Sprintf("Ok. You can push again in %s", g.nextPush.Sub(time.Now())) 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 { } 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)) 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() g.endGame()
} }
} else { } 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 return true
} }

View File

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

View File

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

94
plugins/stock/stock.go Normal file
View File

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

View File

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

View File

@ -4,14 +4,20 @@ package talker
import ( import (
"fmt" "fmt"
"math/rand" "io/ioutil"
"net/http"
"os"
"os/exec"
"strings" "strings"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "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 o a t s e x * g o a t s e x * g o a t s e x *",
"g g", "g g",
"o / \\ \\ / \\ o", "o / \\ \\ / \\ o",
@ -40,28 +46,48 @@ var goatse []string = []string{
} }
type TalkerPlugin struct { type TalkerPlugin struct {
Bot bot.Bot bot bot.Bot
enforceNicks bool config *config.Config
sayings []string sayings []string
} }
func New(bot bot.Bot) *TalkerPlugin { func New(b bot.Bot) *TalkerPlugin {
return &TalkerPlugin{ tp := &TalkerPlugin{
Bot: bot, bot: b,
enforceNicks: bot.Config().EnforceNicks, config: b.Config(),
sayings: bot.Config().WelcomeMsgs,
} }
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 channel := message.Channel
body := message.Body body := message.Body
lowermessage := strings.ToLower(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 // TODO: This ought to be space split afterwards to remove any punctuation
if message.Command && strings.HasPrefix(lowermessage, "say") { if message.Command && strings.HasPrefix(lowermessage, "say") {
msg := strings.TrimSpace(body[3:]) msg := strings.TrimSpace(body[3:])
p.Bot.SendMessage(channel, msg) p.bot.Send(c, bot.Message, channel, msg)
return true return true
} }
@ -77,45 +103,85 @@ func (p *TalkerPlugin) Message(message msg.Message) bool {
line = strings.Replace(line, "{nick}", nick, 1) line = strings.Replace(line, "{nick}", nick, 1)
output += line + "\n" output += line + "\n"
} }
p.Bot.SendMessage(channel, output) p.bot.Send(c, bot.Message, 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)
return true return true
} }
return false return false
} }
func (p *TalkerPlugin) Help(channel string, parts []string) { func (p *TalkerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.SendMessage(channel, "Hi, this is talker. I like to talk about FredFelps!") 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) cowSay(text string) (string, error) {
func (p *TalkerPlugin) Event(kind string, message msg.Message) bool { fields := strings.Split(text, " ")
if kind == "JOIN" && strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) { cow := "default"
if len(p.sayings) == 0 { if len(fields) > 1 && p.hasCow(fields[0]) {
return false 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 return false
} }
// Handler for bot's own messages func (p *TalkerPlugin) allCows() []string {
func (p *TalkerPlugin) BotMessage(message msg.Message) bool { f, err := os.Open(p.config.Get("talker.cowpath", "/usr/local/share/cows"))
return false 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(c bot.Connector) {
func (p *TalkerPlugin) RegisterWeb() *string { http.HandleFunc("/slash/cowsay", func(w http.ResponseWriter, r *http.Request) {
return nil 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 }

View File

@ -3,6 +3,7 @@
package talker package talker
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -29,7 +30,7 @@ func TestGoatse(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("goatse")) res := c.message(makeMessage("goatse"))
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
assert.False(t, res) assert.False(t, res)
} }
@ -38,7 +39,7 @@ func TestGoatseCommand(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!goatse")) res := c.message(makeMessage("!goatse"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e") assert.Contains(t, mb.Messages[0], "g o a t s e")
@ -48,7 +49,7 @@ func TestGoatseWithNickCommand(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!goatse seabass")) res := c.message(makeMessage("!goatse seabass"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e") assert.Contains(t, mb.Messages[0], "g o a t s e")
@ -59,7 +60,7 @@ func TestSay(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("say hello")) res := c.message(makeMessage("say hello"))
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
assert.False(t, res) assert.False(t, res)
} }
@ -68,78 +69,16 @@ func TestSayCommand(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) assert.NotNil(t, c)
res := c.Message(makeMessage("!say hello")) res := c.message(makeMessage("!say hello"))
assert.Len(t, mb.Messages, 1) assert.Len(t, mb.Messages, 1)
assert.True(t, res) assert.True(t, res)
assert.Contains(t, mb.Messages[0], "hello") 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) { func TestHelp(t *testing.T) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) 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) 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

@ -16,32 +16,28 @@ type TellPlugin struct {
} }
func New(b bot.Bot) *TellPlugin { 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") { if strings.HasPrefix(strings.ToLower(message.Body), "tell") {
parts := strings.Split(message.Body, " ") parts := strings.Split(message.Body, " ")
target := strings.ToLower(parts[1]) target := strings.ToLower(parts[1])
newMessage := strings.Join(parts[2:], " ") newMessage := strings.Join(parts[2:], " ")
newMessage = fmt.Sprintf("Hey, %s. %s said: %s", target, message.User.Name, newMessage) newMessage = fmt.Sprintf("Hey, %s. %s said: %s", target, message.User.Name, newMessage)
t.users[target] = append(t.users[target], 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 return true
} }
uname := strings.ToLower(message.User.Name) uname := strings.ToLower(message.User.Name)
if msg, ok := t.users[uname]; ok && len(msg) > 0 { if msg, ok := t.users[uname]; ok && len(msg) > 0 {
for _, m := range msg { 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{} t.users[uname] = []string{}
return true return true
} }
return false 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 }

185
plugins/tldr/badwords.go Normal file
View File

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

180
plugins/tldr/tldr.go Normal file
View File

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

101
plugins/tldr/tldr_test.go Normal file
View File

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

View File

@ -1,30 +1,37 @@
package twitch package twitch
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"text/template"
"time" "time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config" "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 { type TwitchPlugin struct {
Bot bot.Bot bot bot.Bot
config *config.Config config *config.Config
twitchList map[string]*Twitcher twitchList map[string]*Twitcher
} }
type Twitcher struct { type Twitcher struct {
name string name string
game string gameID string
} }
func (t Twitcher) URL() string { func (t Twitcher) URL() string {
@ -51,39 +58,34 @@ type stream struct {
} `json:"pagination"` } `json:"pagination"`
} }
func New(bot bot.Bot) *TwitchPlugin { func New(b bot.Bot) *TwitchPlugin {
p := &TwitchPlugin{ p := &TwitchPlugin{
Bot: bot, bot: b,
config: bot.Config(), config: b.Config(),
twitchList: map[string]*Twitcher{}, twitchList: map[string]*Twitcher{},
} }
for _, users := range p.config.Twitch.Users { for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) {
for _, twitcherName := range users { for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) {
if _, ok := p.twitchList[twitcherName]; !ok { if _, ok := p.twitchList[twitcherName]; !ok {
p.twitchList[twitcherName] = &Twitcher{ p.twitchList[twitcherName] = &Twitcher{
name: twitcherName, name: twitcherName,
game: "", gameID: "",
} }
} }
} }
go p.twitchLoop(b.DefaultConnector(), ch)
} }
for channel := range p.config.Twitch.Users { b.Register(p, bot.Message, p.message)
go p.twitchLoop(channel) b.Register(p, bot.Help, p.help)
} p.registerWeb()
return p return p
} }
func (p *TwitchPlugin) BotMessage(message msg.Message) bool { func (p *TwitchPlugin) registerWeb() {
return false
}
func (p *TwitchPlugin) RegisterWeb() *string {
http.HandleFunc("/isstreaming/", p.serveStreaming) http.HandleFunc("/isstreaming/", p.serveStreaming)
tmp := "/isstreaming"
return &tmp
} }
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) { 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." status := "NO."
if twitcher.game != "" { if twitcher.gameID != "" {
status = "YES." status = "YES."
} }
context := map[string]interface{}{"Name": twitcher.name, "Status": status} context := map[string]interface{}{"Name": twitcher.name, "Status": status}
t, err := template.New("streaming").Parse(page) t, err := template.New("streaming").Parse(page)
if err != nil { if err != nil {
log.Println("Could not parse template!", err) log.Error().Err(err).Msg("Could not parse template!")
return return
} }
err = t.Execute(w, context) err = t.Execute(w, context)
if err != nil { 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 { func (p *TwitchPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "twitch status" { body := strings.ToLower(message.Body)
if body == "twitch status" {
channel := message.Channel channel := message.Channel
if _, ok := p.config.Twitch.Users[channel]; ok { if users := p.config.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
for _, twitcherName := range p.config.Twitch.Users[channel] { for _, twitcherName := range users {
if _, ok = p.twitchList[twitcherName]; ok { if _, ok := p.twitchList[twitcherName]; ok {
p.checkTwitch(channel, p.twitchList[twitcherName], true) p.checkTwitch(c, channel, p.twitchList[twitcherName], true)
} }
} }
} }
return 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 return false
} }
func (p *TwitchPlugin) Event(kind string, message msg.Message) bool { func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
return false 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
}
} log.Info().Msgf("Checking every %d seconds", frequency)
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")
for { for {
time.Sleep(time.Duration(frequency) * time.Second) time.Sleep(time.Duration(frequency) * time.Second)
for _, twitcherName := range p.config.Twitch.Users[channel] { for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) {
p.checkTwitch(channel, p.twitchList[twitcherName], false) p.checkTwitch(c, channel, p.twitchList[twitcherName], false)
} }
} }
} }
@ -184,14 +193,14 @@ func getRequest(url, clientID, authorization string) ([]byte, bool) {
return body, true return body, true
errCase: errCase:
log.Println(err) log.Error().Err(err)
return []byte{}, false 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") baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil { if err != nil {
log.Println("Error parsing twitch stream URL") log.Error().Msg("Error parsing twitch stream URL")
return return
} }
@ -200,8 +209,12 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
baseURL.RawQuery = query.Encode() baseURL.RawQuery = query.Encode()
cid := p.config.Twitch.ClientID cid := p.config.Get("Twitch.ClientID", "")
auth := p.config.Twitch.Authorization 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) body, ok := getRequest(baseURL.String(), cid, auth)
if !ok { if !ok {
@ -211,32 +224,75 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
var s stream var s stream
err = json.Unmarshal(body, &s) err = json.Unmarshal(body, &s)
if err != nil { if err != nil {
log.Println(err) log.Error().Err(err)
return return
} }
games := s.Data games := s.Data
game := "" gameID, title := "", ""
if len(games) > 0 { 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 alwaysPrintStatus {
if game == "" { if gameID == "" {
p.Bot.SendMessage(channel, twitcher.name+" is not streaming.") 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 { } 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 == "" { } else if gameID == "" {
if twitcher.game != "" { if twitcher.gameID != "" {
p.Bot.SendMessage(channel, twitcher.name+" just stopped streaming.") 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 { } else {
if twitcher.game != game { if twitcher.gameID != gameID {
p.Bot.SendMessage(channel, twitcher.name+" just started 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())
} }
twitcher.game = game twitcher.gameID = gameID
} }
} }
func (p *TwitchPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -3,6 +3,7 @@
package twitch package twitch
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, Body: payload,
@ -28,12 +29,15 @@ func makeMessage(payload string) msg.Message {
func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) { func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
mb := bot.NewMockBot() mb := bot.NewMockBot()
c := New(mb) 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) assert.NotNil(t, c)
c.twitchList["drseabass"] = &Twitcher{ c.twitchList["drseabass"] = &Twitcher{
name: "drseabass", name: "drseabass",
game: "", gameID: "",
} }
return c, mb return c, mb
@ -41,6 +45,6 @@ func makeTwitchPlugin(t *testing.T) (*TwitchPlugin, *bot.MockBot) {
func TestTwitch(t *testing.T) { func TestTwitch(t *testing.T) {
b, mb := makeTwitchPlugin(t) b, mb := makeTwitchPlugin(t)
b.Message(makeMessage("!twitch status")) b.message(makeMessage("!twitch status"))
assert.NotEmpty(t, mb.Messages) assert.NotEmpty(t, mb.Messages)
} }

View File

@ -17,52 +17,43 @@ type YourPlugin struct {
} }
// NewYourPlugin creates a new YourPlugin with the Plugin interface // NewYourPlugin creates a new YourPlugin with the Plugin interface
func New(bot bot.Bot) *YourPlugin { func New(b bot.Bot) *YourPlugin {
return &YourPlugin{ yp := &YourPlugin{
bot: bot, bot: b,
config: bot.Config(), 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. // 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. // 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. // Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *YourPlugin) Message(message msg.Message) bool { func (p *YourPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if len(message.Body) > p.config.Your.MaxLength { maxLen := p.config.GetInt("your.maxlength", 140)
if len(message.Body) > maxLen {
return false return false
} }
msg := message.Body msg := message.Body
for _, replacement := range p.config.Your.Replacements { for _, replacement := range p.config.GetArray("Your.Replacements", []string{}) {
if rand.Float64() < replacement.Frequency { freq := p.config.GetFloat64("your.replacements."+replacement+".freq", 0.0)
r := strings.NewReplacer(replacement.This, replacement.That) 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) msg = r.Replace(msg)
} }
} }
if msg != message.Body { if msg != message.Body {
p.bot.SendMessage(message.Channel, msg) p.bot.Send(c, bot.Message, message.Channel, msg)
return true return true
} }
return false return false
} }
// 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 *YourPlugin) Help(channel string, parts []string) { func (p *YourPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.SendMessage(channel, "Your corrects people's grammar.") 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 }

View File

@ -3,6 +3,7 @@
package your package your
import ( import (
"github.com/velour/catbase/plugins/cli"
"strings" "strings"
"testing" "testing"
@ -10,15 +11,14 @@ import (
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user" "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, "!") isCmd := strings.HasPrefix(payload, "!")
if isCmd { if isCmd {
payload = payload[1:] payload = payload[1:]
} }
return msg.Message{ return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"}, User: &user.User{Name: "tester"},
Channel: "test", Channel: "test",
Body: payload, 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() mb := bot.NewMockBot()
c := New(mb) c := New(mb)
assert.NotNil(t, c) mb.DB().MustExec(`delete from config;`)
c.config.Your.MaxLength = 1000 return c, mb
c.config.Your.Replacements = []config.Replacement{ }
config.Replacement{
This: "fuck", func TestReplacement(t *testing.T) {
That: "duck", c, mb := setup(t)
Frequency: 1.0, c.config.Set("Your.MaxLength", "1000")
}, c.config.SetArray("your.replacements", []string{"0"})
} c.config.Set("your.replacements.0.freq", "1.0")
res := c.Message(makeMessage("fuck a duck")) c.config.Set("your.replacements.0.this", "fuck")
assert.Len(t, mb.Messages, 1) c.config.Set("your.replacements.0.that", "duck")
res := c.message(makeMessage("fuck a duck"))
assert.True(t, res) assert.True(t, res)
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "duck a duck") assert.Contains(t, mb.Messages[0], "duck a duck")
} }
func TestNoReplacement(t *testing.T) { func TestNoReplacement(t *testing.T) {
mb := bot.NewMockBot() c, mb := setup(t)
c := New(mb) c.config.Set("Your.MaxLength", "1000")
assert.NotNil(t, c) c.config.SetArray("your.replacements", []string{"0", "1", "2"})
c.config.Your.MaxLength = 1000 c.config.Set("your.replacements.0.freq", "1.0")
c.config.Your.Replacements = []config.Replacement{ c.config.Set("your.replacements.0.this", "nope")
config.Replacement{ c.config.Set("your.replacements.0.that", "duck")
This: "nope",
That: "duck", c.config.Set("your.replacements.1.freq", "1.0")
Frequency: 1.0, c.config.Set("your.replacements.1.this", "nope")
}, c.config.Set("your.replacements.1.that", "duck")
config.Replacement{
This: " fuck", c.config.Set("your.replacements.2.freq", "1.0")
That: "duck", c.config.Set("your.replacements.2.this", "Fuck")
Frequency: 1.0, c.config.Set("your.replacements.2.that", "duck")
}, c.message(makeMessage("fuck a duck"))
config.Replacement{
This: "Fuck",
That: "duck",
Frequency: 1.0,
},
}
c.Message(makeMessage("fuck a duck"))
assert.Len(t, mb.Messages, 0) assert.Len(t, mb.Messages, 0)
} }

View File

@ -8,12 +8,13 @@ import (
"bytes" "bytes"
"go/build" "go/build"
"io" "io"
"log"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot" "github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg" "github.com/velour/catbase/bot/msg"
) )
@ -26,14 +27,17 @@ type ZorkPlugin struct {
zorks map[string]io.WriteCloser zorks map[string]io.WriteCloser
} }
func New(b bot.Bot) bot.Handler { func New(b bot.Bot) bot.Plugin {
return &ZorkPlugin{ z := &ZorkPlugin{
bot: b, bot: b,
zorks: make(map[string]io.WriteCloser), 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" const importString = "github.com/velour/catbase/plugins/zork"
pkg, err := build.Import(importString, "", build.FindOnly) pkg, err := build.Import(importString, "", build.FindOnly)
if err != nil { if err != nil {
@ -49,7 +53,7 @@ func (p *ZorkPlugin) runZork(ch string) error {
var w io.WriteCloser var w io.WriteCloser
cmd.Stdin, w = io.Pipe() 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 { if err := cmd.Start(); err != nil {
w.Close() w.Close()
return err return err
@ -75,25 +79,25 @@ func (p *ZorkPlugin) runZork(ch string) error {
m := strings.Replace(s.Text(), ">", "", -1) m := strings.Replace(s.Text(), ">", "", -1)
m = strings.Replace(m, "\n", "\n>", -1) m = strings.Replace(m, "\n", "\n>", -1)
m = ">" + m + "\n" m = ">" + m + "\n"
p.bot.SendMessage(ch, m) p.bot.Send(c, bot.Message, ch, m)
} }
}() }()
go func() { go func() {
if err := cmd.Wait(); err != nil { if err := cmd.Wait(); err != nil {
log.Printf("zork exited: %v\n", err) log.Error().Err(err).Msg("zork exited")
} }
p.Lock() p.Lock()
p.zorks[ch] = nil p.zorks[ch] = nil
p.Unlock() 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 p.zorks[ch] = w
return nil 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) 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" { if ts := strings.Fields(m); len(ts) < 1 || ts[0] != "zork" {
return false return false
} }
@ -103,24 +107,17 @@ func (p *ZorkPlugin) Message(message msg.Message) bool {
p.Lock() p.Lock()
defer p.Unlock() defer p.Unlock()
if p.zorks[ch] == nil { if p.zorks[ch] == nil {
if err := p.runZork(ch); err != nil { if err := p.runZork(c, ch); err != nil {
p.bot.SendMessage(ch, "failed to run zork: "+err.Error()) p.bot.Send(c, bot.Message, ch, "failed to run zork: "+err.Error())
return true return true
} }
} }
log.Printf("zorking, [%s]\n", m) log.Debug().Msgf("zorking, [%s]", m)
io.WriteString(p.zorks[ch], m+"\n") io.WriteString(p.zorks[ch], m+"\n")
return true return true
} }
func (p *ZorkPlugin) Event(_ string, _ msg.Message) bool { return false } 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 <zork command>'.")
func (p *ZorkPlugin) BotMessage(_ msg.Message) bool { return false } return true
func (p *ZorkPlugin) Help(ch string, _ []string) {
p.bot.SendMessage(ch, "Play zork using 'zork <zork command>'.")
} }
func (p *ZorkPlugin) RegisterWeb() *string { return nil }
func (p *ZorkPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -5,12 +5,14 @@ import (
"flag" "flag"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
var ( var (
@ -20,9 +22,10 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
if *token == "" { if *token == "" {
log.Printf("No token provided.") log.Fatal().Msg("No token provided.")
return return
} }
@ -35,7 +38,7 @@ func main() {
func getFiles() map[string]string { func getFiles() map[string]string {
files := fileResp{} files := fileResp{}
log.Printf("Getting files") log.Debug().Msgf("Getting files")
body := mkReq("https://slack.com/api/emoji.list", body := mkReq("https://slack.com/api/emoji.list",
"token", *token, "token", *token,
) )
@ -43,9 +46,9 @@ func getFiles() map[string]string {
err := json.Unmarshal(body, &files) err := json.Unmarshal(body, &files)
checkErr(err) checkErr(err)
log.Printf("Ok: %v", files.Ok) log.Debug().Msgf("Ok: %v", files.Ok)
if !files.Ok { if !files.Ok {
log.Println(files) log.Debug().Msgf("%+v", files)
} }
return files.Files return files.Files
@ -55,7 +58,7 @@ func downloadFile(n, f string) {
url := strings.Replace(f, "\\", "", -1) // because fuck slack url := strings.Replace(f, "\\", "", -1) // because fuck slack
if strings.HasPrefix(url, "alias:") { if strings.HasPrefix(url, "alias:") {
log.Printf("Skipping alias: %s", url) log.Debug().Msgf("Skipping alias: %s", url)
return return
} }
@ -66,7 +69,7 @@ func downloadFile(n, f string) {
fname := filepath.Join(*path, n+"."+ext) fname := filepath.Join(*path, n+"."+ext)
log.Printf("Downloading from: %s", url) log.Debug().Msgf("Downloading from: %s", url)
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@ -82,18 +85,18 @@ func downloadFile(n, f string) {
defer out.Close() defer out.Close()
io.Copy(out, resp.Body) io.Copy(out, resp.Body)
log.Printf("Downloaded %s", f) log.Debug().Msgf("Downloaded %s", f)
} }
func checkErr(err error) { func checkErr(err error) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
} }
func mkReq(path string, arg ...string) []byte { func mkReq(path string, arg ...string) []byte {
if len(arg)%2 != 0 { if len(arg)%2 != 0 {
log.Fatal("Bad request arg number.") log.Fatal().Msg("Bad request arg number.")
} }
u, err := url.Parse(path) u, err := url.Parse(path)

View File

@ -5,13 +5,15 @@ import (
"flag" "flag"
"io" "io"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
) )
var ( var (
@ -24,6 +26,7 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
for { for {
files, count := getFiles() files, count := getFiles()
@ -40,7 +43,7 @@ func main() {
func getFiles() ([]slackFile, int) { func getFiles() ([]slackFile, int) {
files := fileResp{} files := fileResp{}
log.Printf("Getting files") log.Debug().Msg("Getting files")
body := mkReq("https://slack.com/api/files.list", body := mkReq("https://slack.com/api/files.list",
"token", *token, "token", *token,
"count", strconv.Itoa(*limit), "count", strconv.Itoa(*limit),
@ -50,9 +53,11 @@ func getFiles() ([]slackFile, int) {
err := json.Unmarshal(body, &files) err := json.Unmarshal(body, &files)
checkErr(err) 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 { if !files.Ok {
log.Println(files) log.Error().Interface("files", files)
} }
return files.Files, files.Paging.Pages return files.Files, files.Paging.Pages
@ -69,18 +74,24 @@ func deleteFile(f slackFile) {
checkErr(err) checkErr(err)
if !del.Ok { if !del.Ok {
log.Println(body) log.Fatal().
log.Fatal("Couldn't delete " + f.ID) 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) { func downloadFile(f slackFile) {
url := strings.Replace(f.URLPrivateDownload, "\\", "", -1) // because fuck slack url := strings.Replace(f.URLPrivateDownload, "\\", "", -1) // because fuck slack
fname := filepath.Join(*path, f.ID+f.Name) fname := filepath.Join(*path, f.ID+f.Name)
log.Printf("Downloading from: %s", url) log.Info().
Str("url", url).
Msg("Downloading")
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("GET", url, nil) req, err := http.NewRequest("GET", url, nil)
@ -96,18 +107,20 @@ func downloadFile(f slackFile) {
defer out.Close() defer out.Close()
io.Copy(out, resp.Body) io.Copy(out, resp.Body)
log.Printf("Downloaded %s", f.ID) log.Info().
Str("id", f.ID).
Msg("Downloaded")
} }
func checkErr(err error) { func checkErr(err error) {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal().Err(err)
} }
} }
func mkReq(path string, arg ...string) []byte { func mkReq(path string, arg ...string) []byte {
if len(arg)%2 != 0 { if len(arg)%2 != 0 {
log.Fatal("Bad request arg number.") log.Fatal().Msg("Bad request arg number.")
} }
u, err := url.Parse(path) u, err := url.Parse(path)

View File

@ -1,5 +0,0 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package main
const Version = "0.9"