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/
*.code-workspace
*config.lua
modd.conf
# Created by https://www.gitignore.io/api/macos
# Edit at https://www.gitignore.io/?templates=macos
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# End of https://www.gitignore.io/api/macos
util/*/files
util/*/files
run.sh
.idea
logs
util/files

View File

@ -1,5 +1,7 @@
# CatBase
[![Build Status](https://travis-ci.com/velour/catbase.svg?branch=master)](https://travis-ci.com/velour/catbase)
CatBase is a bot that trolls our little corner of the IRC world and keeps our friends laughing from time to time. Sometimes he makes us angry too. He is crafted as a clone of XKCD's Bucket bot, which learns from things he's told and regurgitates his knowledge to the various channels that he lives in. I've found in many such projects that randomness can often make bots feel much more alive than they are, so CatBase is a big experiment in how great randomness is.
## Getting Help

View File

@ -3,13 +3,15 @@
package bot
import (
"database/sql"
"html/template"
"log"
"fmt"
"math/rand"
"net/http"
"reflect"
"strings"
"time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/msglog"
"github.com/velour/catbase/bot/user"
@ -20,7 +22,7 @@ import (
type bot struct {
// Each plugin must be registered in our plugins handler. To come: a map so that this
// will allow plugins to respond to specific kinds of events
plugins map[string]Handler
plugins map[string]Plugin
pluginOrdering []string
// Users holds information about all of our friends
@ -32,30 +34,33 @@ type bot struct {
conn Connector
// SQL DB
// TODO: I think it'd be nice to use https://github.com/jmoiron/sqlx so that
// the select/update/etc statements could be simplified with struct
// marshalling.
db *sqlx.DB
dbVersion int64
logIn chan msg.Message
logOut chan msg.Messages
version string
// The entries to the bot's HTTP interface
httpEndPoints map[string]string
httpEndPoints []EndPoint
// filters registered by plugins
filters map[string]func(string) string
callbacks CallbackMap
password string
passwordCreated time.Time
}
type EndPoint struct {
Name, URL string
}
// Variable represents a $var replacement
type Variable struct {
Variable, Value string
}
// Newbot creates a bot for a given connection and set of handlers.
// New creates a bot for a given connection and set of handlers.
func New(config *config.Config, connector Connector) Bot {
logIn := make(chan msg.Message)
logOut := make(chan msg.Messages)
@ -63,94 +68,69 @@ func New(config *config.Config, connector Connector) Bot {
msglog.RunNew(logIn, logOut)
users := []user.User{
user.User{
Name: config.Nick,
{
Name: config.Get("Nick", "bot"),
},
}
bot := &bot{
config: config,
plugins: make(map[string]Handler),
plugins: make(map[string]Plugin),
pluginOrdering: make([]string, 0),
conn: connector,
users: users,
me: users[0],
db: config.DBConn,
logIn: logIn,
logOut: logOut,
version: config.Version,
httpEndPoints: make(map[string]string),
httpEndPoints: make([]EndPoint, 0),
filters: make(map[string]func(string) string),
callbacks: make(CallbackMap),
}
bot.migrateDB()
http.HandleFunc("/", bot.serveRoot)
if config.HttpAddr == "" {
config.HttpAddr = "127.0.0.1:1337"
}
go http.ListenAndServe(config.HttpAddr, nil)
connector.RegisterMessageReceived(bot.MsgReceived)
connector.RegisterEventReceived(bot.EventReceived)
connector.RegisterReplyMessageReceived(bot.ReplyMsgReceived)
connector.RegisterEvent(bot.Receive)
return bot
}
func (b *bot) DefaultConnector() Connector {
return b.conn
}
func (b *bot) WhoAmI() string {
return b.me.Name
}
// Config gets the configuration that the bot is using
func (b *bot) Config() *config.Config {
return b.config
}
func (b *bot) DBVersion() int64 {
return b.dbVersion
}
func (b *bot) DB() *sqlx.DB {
return b.db
return b.config.DB
}
// Create any tables if necessary based on version of DB
// Plugins should create their own tables, these are only for official bot stuff
// Note: This does not return an error. Database issues are all fatal at this stage.
func (b *bot) migrateDB() {
_, err := b.db.Exec(`create table if not exists version (version integer);`)
if err != nil {
log.Fatal("Initial DB migration create version table: ", err)
}
var version sql.NullInt64
err = b.db.QueryRow("select max(version) from version").Scan(&version)
if err != nil {
log.Fatal("Initial DB migration get version: ", err)
}
if version.Valid {
b.dbVersion = version.Int64
log.Printf("Database version: %v\n", b.dbVersion)
} else {
log.Printf("No versions, we're the first!.")
_, err := b.db.Exec(`insert into version (version) values (1)`)
if err != nil {
log.Fatal("Initial DB migration insert: ", err)
}
}
if _, err := b.db.Exec(`create table if not exists variables (
if _, err := b.DB().Exec(`create table if not exists variables (
id integer primary key,
name string,
value string
);`); err != nil {
log.Fatal("Initial DB migration create variables table: ", err)
log.Fatal().Err(err).Msgf("Initial DB migration create variables table")
}
}
// Adds a constructed handler to the bots handlers list
func (b *bot) AddHandler(name string, h Handler) {
func (b *bot) AddPlugin(h Plugin) {
name := reflect.TypeOf(h).String()
b.plugins[name] = h
b.pluginOrdering = append(b.pluginOrdering, name)
if entry := h.RegisterWeb(); entry != nil {
b.httpEndPoints[name] = *entry
}
}
func (b *bot) Who(channel string) []user.User {
@ -162,50 +142,14 @@ func (b *bot) Who(channel string) []user.User {
return users
}
var rootIndex string = `
<!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
// IsCmd checks if message is a command and returns its curtailed version
func IsCmd(c *config.Config, message string) (bool, string) {
cmdcs := c.CommandChar
botnick := strings.ToLower(c.Nick)
cmdcs := c.GetArray("CommandChar", []string{"!"})
botnick := strings.ToLower(c.Get("Nick", "bot"))
if botnick == "" {
log.Fatal().
Msgf(`You must run catbase -set nick -val <your bot nick>`)
}
iscmd := false
lowerMessage := strings.ToLower(message)
@ -237,7 +181,7 @@ func IsCmd(c *config.Config, message string) (bool, string) {
}
func (b *bot) CheckAdmin(nick string) bool {
for _, u := range b.Config().Admins {
for _, u := range b.Config().GetArray("Admins", []string{}) {
if nick == u {
return true
}
@ -265,7 +209,7 @@ func (b *bot) NewUser(nick string) *user.User {
}
func (b *bot) checkAdmin(nick string) bool {
for _, u := range b.Config().Admins {
for _, u := range b.Config().GetArray("Admins", []string{}) {
if nick == u {
return true
}
@ -277,3 +221,31 @@ func (b *bot) checkAdmin(nick string) bool {
func (b *bot) RegisterFilter(name string, f func(string) string) {
b.filters[name] = f
}
// Register a callback
func (b *bot) Register(p Plugin, kind Kind, cb Callback) {
t := reflect.TypeOf(p).String()
if _, ok := b.callbacks[t]; !ok {
b.callbacks[t] = make(map[Kind][]Callback)
}
if _, ok := b.callbacks[t][kind]; !ok {
b.callbacks[t][kind] = []Callback{}
}
b.callbacks[t][kind] = append(b.callbacks[t][kind], cb)
}
func (b *bot) RegisterWeb(root, name string) {
b.httpEndPoints = append(b.httpEndPoints, EndPoint{name, root})
}
func (b *bot) GetPassword() string {
if b.passwordCreated.Before(time.Now().Add(-24 * time.Hour)) {
adjs := b.config.GetArray("bot.passwordAdjectives", []string{"very"})
nouns := b.config.GetArray("bot.passwordNouns", []string{"noun"})
verbs := b.config.GetArray("bot.passwordVerbs", []string{"do"})
a, n, v := adjs[rand.Intn(len(adjs))], nouns[rand.Intn(len(nouns))], verbs[rand.Intn(len(verbs))]
b.passwordCreated = time.Now()
b.password = fmt.Sprintf("%s-%s-%s", a, n, v)
}
return b.password
}

View File

@ -6,86 +6,55 @@ import (
"database/sql"
"errors"
"fmt"
"log"
"math/rand"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot/msg"
)
// Handles incomming PRIVMSG requests
func (b *bot) MsgReceived(msg msg.Message) {
log.Println("Received message: ", msg)
func (b *bot) Receive(conn Connector, kind Kind, msg msg.Message, args ...interface{}) bool {
log.Debug().
Interface("msg", msg).
Msg("Received event")
// msg := b.buildMessage(client, inMsg)
// do need to look up user and fix it
if strings.HasPrefix(msg.Body, "help ") && msg.Command {
if kind == Message && strings.HasPrefix(msg.Body, "help") && msg.Command {
parts := strings.Fields(strings.ToLower(msg.Body))
b.checkHelp(msg.Channel, parts)
b.checkHelp(conn, msg.Channel, parts)
log.Debug().Msg("Handled a help, returning")
goto RET
}
for _, name := range b.pluginOrdering {
p := b.plugins[name]
if p.Message(msg) {
break
if b.runCallback(conn, b.plugins[name], kind, msg, args...) {
goto RET
}
}
RET:
b.logIn <- msg
return
return true
}
// Handle incoming events
func (b *bot) EventReceived(msg msg.Message) {
log.Println("Received event: ", msg)
//msg := b.buildMessage(conn, inMsg)
for _, name := range b.pluginOrdering {
p := b.plugins[name]
if p.Event(msg.Body, msg) { // TODO: could get rid of msg.Body
break
func (b *bot) runCallback(conn Connector, plugin Plugin, evt Kind, message msg.Message, args ...interface{}) bool {
t := reflect.TypeOf(plugin).String()
for _, cb := range b.callbacks[t][evt] {
if cb(conn, evt, message, args...) {
return true
}
}
return false
}
// Handle incoming replys
func (b *bot) ReplyMsgReceived(msg msg.Message, identifier string) {
log.Println("Received message: ", msg)
for _, name := range b.pluginOrdering {
p := b.plugins[name]
if p.ReplyMessage(msg, identifier) {
break
}
}
}
func (b *bot) SendMessage(channel, message string) string {
return b.conn.SendMessage(channel, message)
}
func (b *bot) SendAction(channel, message string) string {
return b.conn.SendAction(channel, message)
}
func (b *bot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
return b.conn.ReplyToMessageIdentifier(channel, message, identifier)
}
func (b *bot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
return b.conn.ReplyToMessage(channel, message, replyTo)
}
func (b *bot) React(channel, reaction string, message msg.Message) bool {
return b.conn.React(channel, reaction, message)
}
func (b *bot) Edit(channel, newMessage, identifier string) bool {
return b.conn.Edit(channel, newMessage, identifier)
// Send a message to the connection
func (b *bot) Send(conn Connector, kind Kind, args ...interface{}) (string, error) {
return conn.Send(kind, args...)
}
func (b *bot) GetEmojiList() map[string]string {
@ -93,31 +62,38 @@ func (b *bot) GetEmojiList() map[string]string {
}
// Checks to see if the user is asking for help, returns true if so and handles the situation.
func (b *bot) checkHelp(channel string, parts []string) {
func (b *bot) checkHelp(conn Connector, channel string, parts []string) {
if len(parts) == 1 {
// just print out a list of help topics
topics := "Help topics: about variables"
for name, _ := range b.plugins {
for name := range b.plugins {
name = strings.Split(strings.TrimPrefix(name, "*"), ".")[0]
topics = fmt.Sprintf("%s, %s", topics, name)
}
b.SendMessage(channel, topics)
b.Send(conn, Message, channel, topics)
} else {
// trigger the proper plugin's help response
if parts[1] == "about" {
b.Help(channel, parts)
b.Help(conn, channel, parts)
return
}
if parts[1] == "variables" {
b.listVars(channel, parts)
b.listVars(conn, channel, parts)
return
}
plugin := b.plugins[parts[1]]
if plugin != nil {
plugin.Help(channel, parts)
} else {
msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", parts[1])
b.SendMessage(channel, msg)
for name, plugin := range b.plugins {
if strings.HasPrefix(name, "*"+parts[1]) {
if b.runCallback(conn, plugin, Help, msg.Message{Channel: channel}, channel, parts) {
return
} else {
msg := fmt.Sprintf("I'm sorry, I don't know how to help you with %s.", parts[1])
b.Send(conn, Message, channel, msg)
return
}
}
}
msg := fmt.Sprintf("I'm sorry, I don't know what %s is!", strings.Join(parts, " "))
b.Send(conn, Message, channel, msg)
}
}
@ -192,38 +168,38 @@ func (b *bot) Filter(message msg.Message, input string) string {
func (b *bot) getVar(varName string) (string, error) {
var text string
err := b.db.Get(&text, `select value from variables where name=? order by random() limit 1`, varName)
err := b.DB().Get(&text, `select value from variables where name=? order by random() limit 1`, varName)
switch {
case err == sql.ErrNoRows:
return "", fmt.Errorf("No factoid found")
case err != nil:
log.Fatal("getVar error: ", err)
log.Fatal().Err(err).Msg("getVar error")
}
return text, nil
}
func (b *bot) listVars(channel string, parts []string) {
func (b *bot) listVars(conn Connector, channel string, parts []string) {
var variables []string
err := b.db.Select(&variables, `select name from variables group by name`)
err := b.DB().Select(&variables, `select name from variables group by name`)
if err != nil {
log.Fatal(err)
log.Fatal().Err(err)
}
msg := "I know: $who, $someone, $digit, $nonzero"
if len(variables) > 0 {
msg += ", " + strings.Join(variables, ", ")
}
b.SendMessage(channel, msg)
b.Send(conn, Message, channel, msg)
}
func (b *bot) Help(channel string, parts []string) {
func (b *bot) Help(conn Connector, channel string, parts []string) {
msg := fmt.Sprintf("Hi, I'm based on godeepintir version %s. I'm written in Go, and you "+
"can find my source code on the internet here: "+
"http://github.com/velour/catbase", b.version)
b.SendMessage(channel, msg)
b.Send(conn, Message, channel, msg)
}
// Send our own musings to the plugins
func (b *bot) selfSaid(channel, message string, action bool) {
func (b *bot) selfSaid(conn Connector, channel, message string, action bool) {
msg := msg.Message{
User: &b.me, // hack
Channel: channel,
@ -236,9 +212,8 @@ func (b *bot) selfSaid(channel, message string, action bool) {
}
for _, name := range b.pluginOrdering {
p := b.plugins[name]
if p.BotMessage(msg) {
break
if b.runCallback(conn, b.plugins[name], SelfMessage, msg) {
return
}
}
}

View File

@ -9,51 +9,80 @@ import (
"github.com/velour/catbase/config"
)
const (
_ = iota
// Message any standard chat
Message
// Reply something containing a message reference
Reply
// Action any /me action
Action
// Reaction Icon reaction if service supports it
Reaction
// Edit message ref'd new message to replace
Edit
// Not sure what event is
Event
// Help is used when the bot help system is triggered
Help
// SelfMessage triggers when the bot is sending a message
SelfMessage
)
type ImageAttachment struct {
URL string
AltTxt string
}
type Kind int
type Callback func(Connector, Kind, msg.Message, ...interface{}) bool
type CallbackMap map[string]map[Kind][]Callback
// Bot interface serves to allow mocking of the actual bot
type Bot interface {
// Config allows access to the bot's configuration system
Config() *config.Config
DBVersion() int64
// DB gives access to the current database
DB() *sqlx.DB
// Who lists users in a particular channel
Who(string) []user.User
AddHandler(string, Handler)
SendMessage(string, string) string
SendAction(string, string) string
ReplyToMessageIdentifier(string, string, string) (string, bool)
ReplyToMessage(string, string, msg.Message) (string, bool)
React(string, string, msg.Message) bool
Edit(string, string, string) bool
MsgReceived(msg.Message)
ReplyMsgReceived(msg.Message, string)
EventReceived(msg.Message)
// WhoAmI gives a nick for the bot
WhoAmI() string
// AddPlugin registers a new plugin handler
AddPlugin(Plugin)
// First arg should be one of bot.Message/Reply/Action/etc
Send(Connector, Kind, ...interface{}) (string, error)
// First arg should be one of bot.Message/Reply/Action/etc
Receive(Connector, Kind, msg.Message, ...interface{}) bool
// Register a callback
Register(Plugin, Kind, Callback)
Filter(msg.Message, string) string
LastMessage(string) (msg.Message, error)
CheckAdmin(string) bool
GetEmojiList() map[string]string
RegisterFilter(string, func(string) string)
RegisterWeb(string, string)
DefaultConnector() Connector
GetWebNavigation() []EndPoint
GetPassword() string
}
// Connector represents a server connection to a chat service
type Connector interface {
RegisterEventReceived(func(message msg.Message))
RegisterMessageReceived(func(message msg.Message))
RegisterReplyMessageReceived(func(msg.Message, string))
RegisterEvent(Callback)
Send(Kind, ...interface{}) (string, error)
SendMessage(channel, message string) string
SendAction(channel, message string) string
ReplyToMessageIdentifier(string, string, string) (string, bool)
ReplyToMessage(string, string, msg.Message) (string, bool)
React(string, string, msg.Message) bool
Edit(string, string, string) bool
GetEmojiList() map[string]string
Serve() error
Who(string) []string
}
// Interface used for compatibility with the Plugin interface
type Handler interface {
Message(message msg.Message) bool
Event(kind string, message msg.Message) bool
ReplyMessage(msg.Message, string) bool
BotMessage(message msg.Message) bool
Help(channel string, parts []string)
RegisterWeb() *string
// Plugin interface used for compatibility with the Plugin interface
// Uhh it turned empty, but we're still using it to ID plugins
type Plugin interface {
}

View File

@ -4,11 +4,12 @@ package bot
import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/mock"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/bot/user"
@ -19,85 +20,94 @@ type MockBot struct {
mock.Mock
db *sqlx.DB
Cfg config.Config
Cfg *config.Config
Messages []string
Actions []string
Messages []string
Actions []string
Reactions []string
}
func (mb *MockBot) Config() *config.Config { return &mb.Cfg }
func (mb *MockBot) DBVersion() int64 { return 1 }
func (mb *MockBot) DB() *sqlx.DB { return mb.db }
func (mb *MockBot) Conn() Connector { return nil }
func (mb *MockBot) Who(string) []user.User { return []user.User{} }
func (mb *MockBot) AddHandler(name string, f Handler) {}
func (mb *MockBot) SendMessage(ch string, msg string) string {
mb.Messages = append(mb.Messages, msg)
return fmt.Sprintf("m-%d", len(mb.Actions)-1)
func (mb *MockBot) Config() *config.Config { return mb.Cfg }
func (mb *MockBot) DB() *sqlx.DB { return mb.Cfg.DB }
func (mb *MockBot) Who(string) []user.User { return []user.User{} }
func (mb *MockBot) WhoAmI() string { return "tester" }
func (mb *MockBot) DefaultConnector() Connector { return nil }
func (mb *MockBot) GetPassword() string { return "12345" }
func (mb *MockBot) Send(c Connector, kind Kind, args ...interface{}) (string, error) {
switch kind {
case Message:
mb.Messages = append(mb.Messages, args[1].(string))
return fmt.Sprintf("m-%d", len(mb.Actions)-1), nil
case Action:
mb.Actions = append(mb.Actions, args[1].(string))
return fmt.Sprintf("a-%d", len(mb.Actions)-1), nil
case Edit:
ch, m, id := args[0].(string), args[1].(string), args[2].(string)
return mb.edit(c, ch, m, id)
case Reaction:
ch, re, msg := args[0].(string), args[1].(string), args[2].(msg.Message)
return mb.react(c, ch, re, msg)
}
return "ERR", fmt.Errorf("Mesasge type unhandled")
}
func (mb *MockBot) SendAction(ch string, msg string) string {
mb.Actions = append(mb.Actions, msg)
return fmt.Sprintf("a-%d", len(mb.Actions)-1)
func (mb *MockBot) AddPlugin(f Plugin) {}
func (mb *MockBot) Register(p Plugin, kind Kind, cb Callback) {}
func (mb *MockBot) RegisterWeb(_, _ string) {}
func (mb *MockBot) GetWebNavigation() []EndPoint { return nil }
func (mb *MockBot) Receive(c Connector, kind Kind, msg msg.Message, args ...interface{}) bool {
return false
}
func (mb *MockBot) ReplyToMessageIdentifier(channel, message, identifier string) (string, bool) {
return "", false
}
func (mb *MockBot) ReplyToMessage(channel, message string, replyTo msg.Message) (string, bool) {
return "", false
}
func (mb *MockBot) MsgReceived(msg msg.Message) {}
func (mb *MockBot) EventReceived(msg msg.Message) {}
func (mb *MockBot) Filter(msg msg.Message, s string) string { return "" }
func (mb *MockBot) Filter(msg msg.Message, s string) string { return s }
func (mb *MockBot) LastMessage(ch string) (msg.Message, error) { return msg.Message{}, nil }
func (mb *MockBot) CheckAdmin(nick string) bool { return false }
func (mb *MockBot) React(channel, reaction string, message msg.Message) bool { return false }
func (mb *MockBot) react(c Connector, channel, reaction string, message msg.Message) (string, error) {
mb.Reactions = append(mb.Reactions, reaction)
return "", nil
}
func (mb *MockBot) Edit(channel, newMessage, identifier string) bool {
func (mb *MockBot) edit(c Connector, channel, newMessage, identifier string) (string, error) {
isMessage := identifier[0] == 'm'
if !isMessage && identifier[0] != 'a' {
log.Printf("failed to parse identifier: %s", identifier)
return false
err := fmt.Errorf("failed to parse identifier: %s", identifier)
log.Error().Err(err)
return "", err
}
index, err := strconv.Atoi(strings.Split(identifier, "-")[1])
if err != nil {
log.Printf("failed to parse identifier: %s", identifier)
return false
err := fmt.Errorf("failed to parse identifier: %s", identifier)
log.Error().Err(err)
return "", err
}
if isMessage {
if index < len(mb.Messages) {
mb.Messages[index] = newMessage
} else {
return false
return "", fmt.Errorf("No message")
}
} else {
if index < len(mb.Actions) {
mb.Actions[index] = newMessage
} else {
return false
return "", fmt.Errorf("No action")
}
}
return true
}
func (mb *MockBot) ReplyMsgReceived(msg.Message, string) {
return "", nil
}
func (mb *MockBot) GetEmojiList() map[string]string { return make(map[string]string) }
func (mb *MockBot) RegisterFilter(s string, f func(string) string) {}
func NewMockBot() *MockBot {
db, err := sqlx.Open("sqlite3_custom", ":memory:")
if err != nil {
log.Fatal("Failed to open database:", err)
}
cfg := config.ReadConfig("file::memory:?mode=memory&cache=shared")
b := MockBot{
db: db,
Cfg: cfg,
Messages: make([]string, 0),
Actions: make([]string, 0),
}
// If any plugin registered a route, we need to reset those before any new test
http.DefaultServeMux = new(http.ServeMux)
return &b
}

View File

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

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 (
"database/sql"
"fmt"
"log"
"os"
"regexp"
"strconv"
"strings"
"github.com/jmoiron/sqlx"
sqlite3 "github.com/mattn/go-sqlite3"
"github.com/yuin/gluamapper"
lua "github.com/yuin/gopher-lua"
"github.com/mattn/go-sqlite3"
"github.com/rs/zerolog/log"
)
// Config stores any system-wide startup information that cannot be easily configured via
// the database
type Config struct {
DBConn *sqlx.DB
*sqlx.DB
DB struct {
File string
Name string
Server string
DBFile string
}
// GetFloat64 returns the config value for a string key
// It will first look in the env vars for the key
// It will check the DB for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will attempt to convert the value to a float64 if it exists
func (c *Config) GetFloat64(key string, fallback float64) float64 {
f, err := strconv.ParseFloat(c.GetString(key, fmt.Sprintf("%f", fallback)), 64)
if err != nil {
return 0.0
}
Channels []string
MainChannel string
Plugins []string
Type string
Irc struct {
Server, Pass string
return f
}
// GetInt returns the config value for a string key
// It will first look in the env vars for the key
// It will check the DB for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will attempt to convert the value to an int if it exists
func (c *Config) GetInt(key string, fallback int) int {
i, err := strconv.Atoi(c.GetString(key, strconv.Itoa(fallback)))
if err != nil {
return 0
}
Slack struct {
Token string
return i
}
// Get is a shortcut for GetString
func (c *Config) Get(key, fallback string) string {
return c.GetString(key, fallback)
}
func envkey(key string) string {
key = strings.ToUpper(key)
key = strings.Replace(key, ".", "", -1)
return key
}
// GetString returns the config value for a string key
// It will first look in the env vars for the key
// It will check the DB for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// It will convert the value to a string if it exists
func (c *Config) GetString(key, fallback string) string {
key = strings.ToLower(key)
if v, found := os.LookupEnv(envkey(key)); found {
return v
}
Nick string
IconURL string
FullName string
Version string
CommandChar []string
RatePerSec float64
LogLength int
Admins []string
HttpAddr string
Untappd struct {
Token string
Freq int
Channels []string
var configValue string
q := `select value from config where key=?`
err := c.DB.Get(&configValue, q, key)
if err != nil {
log.Debug().Msgf("WARN: Key %s is empty", key)
return fallback
}
Twitch struct {
Freq int
Users map[string][]string //channel -> usernames
ClientID string
Authorization string
return configValue
}
// GetArray returns the string slice config value for a string key
// It will first look in the env vars for the key with ;; separated values
// Look, I'm too lazy to do parsing to ensure that a comma is what the user meant
// It will check the DB for the key if an env DNE
// Finally, it will return a zero value if the key does not exist
// This will do no conversion.
func (c *Config) GetArray(key string, fallback []string) []string {
val := c.GetString(key, "")
if val == "" {
return fallback
}
EnforceNicks bool
WelcomeMsgs []string
TwitterConsumerKey string
TwitterConsumerSecret string
TwitterUserKey string
TwitterUserSecret string
BadMsgs []string
Bad struct {
Msgs []string
Nicks []string
Hosts []string
return strings.Split(val, ";;")
}
// Set changes the value for a configuration in the database
// Note, this is always a string. Use the SetArray for an array helper
func (c *Config) Set(key, value string) error {
key = strings.ToLower(key)
q := `insert into config (key,value) values (?, ?)
on conflict(key) do update set value=?;`
tx, err := c.Begin()
if err != nil {
return err
}
Your struct {
MaxLength int
Replacements []Replacement
_, err = tx.Exec(q, key, value, value)
if err != nil {
return err
}
LeftPad struct {
MaxLen int
Who string
}
Factoid struct {
MinLen int
QuoteChance float64
QuoteTime int
StartupFact string
}
Babbler struct {
DefaultUsers []string
}
Reminder struct {
MaxBatchAdd int
}
Stats struct {
DBPath string
Sightings []string
}
Emojify struct {
Chance float64
Scoreless []string
}
Reaction struct {
GeneralChance float64
HarrassChance float64
NegativeHarrassmentMultiplier int
HarrassList []string
PositiveReactions []string
NegativeReactions []string
}
Inventory struct {
Max int
}
Sisyphus struct {
MinDecrement int
MaxDecrement int
MinPush int
MaxPush int
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (c *Config) SetArray(key string, values []string) error {
vals := strings.Join(values, ";;")
return c.Set(key, vals)
}
func init() {
@ -125,38 +132,31 @@ func init() {
})
}
type Replacement struct {
This string
That string
Frequency float64
}
// Readconfig loads the config data out of a JSON file located in cfile
func Readconfig(version, cfile string) *Config {
fmt.Printf("Using %s as config file.\n", cfile)
L := lua.NewState()
if err := L.DoFile(cfile); err != nil {
panic(err)
func ReadConfig(dbpath string) *Config {
if dbpath == "" {
dbpath = "catbase.db"
}
log.Info().Msgf("Using %s as database file.\n", dbpath)
var c Config
if err := gluamapper.Map(L.GetGlobal("config").(*lua.LTable), &c); err != nil {
panic(err)
}
c.Version = version
if c.Type == "" {
c.Type = "irc"
}
fmt.Printf("godeepintir version %s running.\n", c.Version)
sqlDB, err := sqlx.Open("sqlite3_custom", c.DB.File)
sqlDB, err := sqlx.Open("sqlite3_custom", dbpath)
if err != nil {
log.Fatal(err)
log.Fatal().Err(err)
}
c.DBConn = sqlDB
c := Config{
DBFile: dbpath,
}
c.DB = sqlDB
if _, err := c.Exec(`create table if not exists config (
key string,
value string,
primary key (key)
);`); err != nil {
panic(err)
}
log.Info().Msgf("catbase is running.")
return &c
}

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

View File

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

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
require (
github.com/PuerkitoBio/goquery v1.5.0 // indirect
github.com/boltdb/bolt v1.3.1
github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c
github.com/PuerkitoBio/goquery v1.5.0
github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect
github.com/fogleman/gg v1.3.0 // indirect
github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/golang/protobuf v1.3.2 // indirect
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 // indirect
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 // indirect
github.com/jmoiron/sqlx v1.2.0
github.com/mattn/go-sqlite3 v1.10.0
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/jung-kurt/gofpdf v1.7.0 // indirect
github.com/lib/pq v1.2.0 // indirect
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 // indirect
github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018 // indirect
github.com/mattn/go-sqlite3 v1.11.0
github.com/mmcdole/gofeed v1.0.0-beta2
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/nlopes/slack v0.5.0
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
github.com/rs/zerolog v1.15.0
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/objx v0.2.0 // indirect
github.com/stretchr/testify v1.3.0
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec
golang.org/x/net v0.0.0-20181217023233-e147a9138326 // indirect
golang.org/x/text v0.3.0 // indirect
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
golang.org/x/mobile v0.0.0-20190806162312-597adff16ade // indirect
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect
golang.org/x/tools v0.0.0-20190813142322-97f12d73768f // indirect
gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b // indirect
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e // indirect
gonum.org/v1/plot v0.0.0-20190615073203-9aa86143727f // indirect
google.golang.org/appengine v1.6.1 // indirect
gopkg.in/sourcemap.v1 v1.0.5 // indirect
)

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/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/ajstarks/svgo v0.0.0-20181006003313-6ce6a3bcf6cd/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff h1:+TEqaP0eO1unI7XHHFeMDhsxhLDIb0x8KYuZbqbAmxA=
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff/go.mod h1:QCRjR0b4qiJiNjuP7RFM89bh4UExGJalcWmYeSvlnRc=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82 h1:EvokxLQsaaQjcWVWSV38221VAK7qc2zhaO17bKys/18=
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029 h1:8jtTdc+Nfj9AR+0soOeia9UZSvYBvETVHZrugUowJ7M=
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/james-bowman/nlp v0.0.0-20190301165020-c5645f996605 h1:MjLvsJmW4uoTjleqqiL0wKRTjxUakKUhDNoSsSlS2hk=
github.com/james-bowman/nlp v0.0.0-20190301165020-c5645f996605/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889 h1:VYwE/yKDYXpd5hno5fCCWv2wPcM37DYAX4r3Re1pLz8=
github.com/james-bowman/nlp v0.0.0-20190408090549-143ee6f41889/go.mod h1:kixuaexEqWB+mHZNysgnb6mqgGIT25WvD1/tFRRt0J0=
github.com/james-bowman/sparse v0.0.0-20190309194602-7d83420cfcbe h1:UFAsFuH6cu/0Lx+qBWfxiO69jrPkvdbG3qwSWI/7yF0=
github.com/james-bowman/sparse v0.0.0-20190309194602-7d83420cfcbe/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7 h1:ph/BDQQDL41apnHSN48I5GyNOQXXAlc79HwGqDSXCss=
github.com/james-bowman/sparse v0.0.0-20190423065201-80c6877364c7/go.mod h1:G6EcQnwZKsWtItoaQHd+FHPPk6bDeYVJSeeSP9Sge+I=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.7.0/go.mod h1:s/VXv+TdctEOx2wCEguezYaR7f0OwUAd6H9VGfRkcSs=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5 h1:AsEBgzv3DhuYHI/GiQh2HxvTP71HCCE9E/tzGUzGdtU=
github.com/lusis/go-slackbot v0.0.0-20180109053408-401027ccfef5/go.mod h1:c2mYKRyMb1BPkO5St0c/ps62L4S0W2NAkaTXj9qEI+0=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6 h1:iOAVXzZyXtW408TMYejlUPo6BIn92HmOacWtIfNyYns=
github.com/lusis/slack-test v0.0.0-20180109053238-3c758769bfa6/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
github.com/lusis/slack-test v0.0.0-20190426140909-c40012f20018/go.mod h1:sFlOUpQL1YcjhFVXhg1CG8ZASEs/Mf1oVb6H75JL/zg=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q=
github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mmcdole/gofeed v1.0.0-beta2 h1:CjQ0ADhAwNSb08zknAkGOEYqr8zfZKfrzgk9BxpWP2E=
github.com/mmcdole/gofeed v1.0.0-beta2/go.mod h1:/BF9JneEL2/flujm8XHoxUcghdTV6vvb3xx/vKyChFU=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI=
github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8=
github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
github.com/olebedev/when v0.0.0-20190131080308-164b69386514 h1:xpZutaUgtGPKT2JFaH72/yby908QS9ORlnrAkkdJ4m0=
github.com/olebedev/when v0.0.0-20190131080308-164b69386514/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254 h1:JYoQR67E1vv1WGoeW8DkdFs7vrIEe/5wP+qJItd5tUE=
github.com/olebedev/when v0.0.0-20190311101825-c3b538a97254/go.mod h1:DPucAeQGDPUzYUt+NaWw6qsF5SFapWWToxEiVDh2aV0=
github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.12.0 h1:aqZ1XRadoS8IBknR5IDFvGzbHly1X9ApIqOroooQF/c=
github.com/rs/zerolog v1.12.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89 h1:3D3M900hEBJJAqyKl70QuRHi5weX9+ptlQI1v+FNcQ8=
github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89/go.mod h1:ejwOYCjnDMyO5LXFXRARQJGBZ6xQJZ3rgAHE5drSuMM=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 h1:p3rTUXxzuKsBOsHlkly7+rj9wagFBKeIsCDKkDII9sw=
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158/go.mod h1:Ojy3BTOiOTwpHpw7/HNi+TVTFppao191PQs+Qc3sqpE=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 h1:noHsffKZsNfU38DwcXWEPldrTjIZ8FPNKx8mYMGnqjs=
github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7/go.mod h1:bbMEM6aU1WDF1ErA5YJ0p91652pGv140gGw4Ww3RGp8=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec h1:vpF8Kxql6/3OvGH4y2SKtpN3WsB17mvJ8f8H1o2vucQ=
github.com/yuin/gopher-lua v0.0.0-20181214045814-db9ae37725ec/go.mod h1:fFiAh+CowNFr0NK5VASokuwKwkbacRmHsVA7Yb1Tqac=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 h1:Ep4L2ibjtJcW6IP73KbcJAU0cpNKsLNSSP2jE1xlCys=
golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190806162312-597adff16ade/go.mod h1:AlhUtkH4DA4asiFC5RgK7ZKmauvtkAVcy9L0epCzlWo=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181217023233-e147a9138326 h1:iCzOf0xz39Tstp+Tu/WwyGjUXCk34QhQORRxBeXXTA4=
golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0=
golang.org/x/net v0.0.0-20190606173856-1492cefac77f/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190813142322-97f12d73768f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.0.0-20190325211145-e42c1265cdd5 h1:tyvqqvbB9Sn6UPjokEzsK6cCE9k4Tx/AHGGaJiLIk7g=
gonum.org/v1/gonum v0.0.0-20190325211145-e42c1265cdd5/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b h1:wlZ2AJblZitrh7dfm5OX2WenXLBZCuWqUeNczop2lPA=
gonum.org/v1/gonum v0.0.0-20190808205415-ced62fe5104b/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gonum.org/v1/plot v0.0.0-20190615073203-9aa86143727f/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

120
main.go
View File

@ -4,19 +4,26 @@ package main
import (
"flag"
"log"
"github.com/velour/catbase/plugins/cli"
"github.com/velour/catbase/plugins/newsbid"
"math/rand"
"net/http"
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/config"
"github.com/velour/catbase/irc"
"github.com/velour/catbase/connectors/irc"
"github.com/velour/catbase/connectors/slack"
"github.com/velour/catbase/connectors/slackapp"
"github.com/velour/catbase/plugins/admin"
"github.com/velour/catbase/plugins/babbler"
"github.com/velour/catbase/plugins/beers"
"github.com/velour/catbase/plugins/couldashouldawoulda"
"github.com/velour/catbase/plugins/counter"
"github.com/velour/catbase/plugins/db"
"github.com/velour/catbase/plugins/dice"
"github.com/velour/catbase/plugins/emojifyme"
"github.com/velour/catbase/plugins/fact"
@ -26,68 +33,107 @@ import (
"github.com/velour/catbase/plugins/nerdepedia"
"github.com/velour/catbase/plugins/picker"
"github.com/velour/catbase/plugins/reaction"
"github.com/velour/catbase/plugins/remember"
"github.com/velour/catbase/plugins/reminder"
"github.com/velour/catbase/plugins/rpgORdie"
"github.com/velour/catbase/plugins/rss"
"github.com/velour/catbase/plugins/sisyphus"
"github.com/velour/catbase/plugins/stock"
"github.com/velour/catbase/plugins/talker"
"github.com/velour/catbase/plugins/tell"
"github.com/velour/catbase/plugins/tldr"
"github.com/velour/catbase/plugins/twitch"
"github.com/velour/catbase/plugins/your"
"github.com/velour/catbase/plugins/zork"
"github.com/velour/catbase/slack"
)
var (
key = flag.String("set", "", "Configuration key to set")
val = flag.String("val", "", "Configuration value to set")
initDB = flag.Bool("init", false, "Initialize the configuration DB")
prettyLog = flag.Bool("pretty", false, "Use pretty console logger")
debug = flag.Bool("debug", false, "Turn on debug logging")
)
func main() {
rand.Seed(time.Now().Unix())
var cfile = flag.String("config", "config.lua",
"Config file to load. (Defaults to config.lua)")
var dbpath = flag.String("db", "catbase.db",
"Database file to load. (Defaults to catbase.db)")
flag.Parse() // parses the logging flags.
c := config.Readconfig(Version, *cfile)
log.Logger = log.With().Caller().Stack().Logger()
if *prettyLog {
log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
if *debug {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
c := config.ReadConfig(*dbpath)
if *key != "" && *val != "" {
c.Set(*key, *val)
log.Info().Msgf("Set config %s: %s", *key, *val)
return
}
if (*initDB && len(flag.Args()) != 2) || (!*initDB && c.GetInt("init", 0) != 1) {
log.Fatal().Msgf(`You must run "catbase -init <channel> <nick>"`)
} else if *initDB {
c.SetDefaults(flag.Arg(0), flag.Arg(1))
return
}
var client bot.Connector
switch c.Type {
switch c.Get("type", "slackapp") {
case "irc":
client = irc.New(c)
case "slack":
client = slack.New(c)
case "slackapp":
client = slackapp.New(c)
default:
log.Fatalf("Unknown connection type: %s", c.Type)
log.Fatal().Msgf("Unknown connection type: %s", c.Get("type", "UNSET"))
}
b := bot.New(c, client)
b.AddHandler("admin", admin.New(b))
b.AddHandler("first", first.New(b))
b.AddHandler("leftpad", leftpad.New(b))
b.AddHandler("talker", talker.New(b))
b.AddHandler("dice", dice.New(b))
b.AddHandler("picker", picker.New(b))
b.AddHandler("beers", beers.New(b))
b.AddHandler("remember", fact.NewRemember(b))
b.AddHandler("your", your.New(b))
b.AddHandler("counter", counter.New(b))
b.AddHandler("reminder", reminder.New(b))
b.AddHandler("babbler", babbler.New(b))
b.AddHandler("zork", zork.New(b))
b.AddHandler("rss", rss.New(b))
b.AddHandler("reaction", reaction.New(b))
b.AddHandler("emojifyme", emojifyme.New(b))
b.AddHandler("twitch", twitch.New(b))
b.AddHandler("inventory", inventory.New(b))
b.AddHandler("rpgORdie", rpgORdie.New(b))
b.AddHandler("sisyphus", sisyphus.New(b))
b.AddHandler("tell", tell.New(b))
b.AddHandler("couldashouldawoulda", couldashouldawoulda.New(b))
b.AddHandler("nedepedia", nerdepedia.New(b))
b.AddPlugin(admin.New(b))
b.AddPlugin(emojifyme.New(b))
b.AddPlugin(first.New(b))
b.AddPlugin(leftpad.New(b))
b.AddPlugin(talker.New(b))
b.AddPlugin(dice.New(b))
b.AddPlugin(picker.New(b))
b.AddPlugin(beers.New(b))
b.AddPlugin(remember.New(b))
b.AddPlugin(your.New(b))
b.AddPlugin(counter.New(b))
b.AddPlugin(reminder.New(b))
b.AddPlugin(babbler.New(b))
b.AddPlugin(zork.New(b))
b.AddPlugin(rss.New(b))
b.AddPlugin(reaction.New(b))
b.AddPlugin(twitch.New(b))
b.AddPlugin(inventory.New(b))
b.AddPlugin(rpgORdie.New(b))
b.AddPlugin(sisyphus.New(b))
b.AddPlugin(tell.New(b))
b.AddPlugin(couldashouldawoulda.New(b))
b.AddPlugin(nerdepedia.New(b))
b.AddPlugin(tldr.New(b))
b.AddPlugin(stock.New(b))
b.AddPlugin(newsbid.New(b))
b.AddPlugin(cli.New(b))
// catches anything left, will always return true
b.AddHandler("factoid", fact.New(b))
b.AddHandler("db", db.New(b))
b.AddPlugin(fact.New(b))
for {
err := client.Serve()
log.Println(err)
if err := client.Serve(); err != nil {
log.Fatal().Err(err)
}
addr := c.Get("HttpAddr", "127.0.0.1:1337")
log.Fatal().Err(http.ListenAndServe(addr, nil))
}

View File

@ -3,55 +3,121 @@
package admin
import (
"log"
"encoding/json"
"fmt"
"html/template"
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
)
// This is a admin plugin to serve as an example and quick copy/paste for new plugins.
type AdminPlugin struct {
Bot bot.Bot
bot bot.Bot
db *sqlx.DB
cfg *config.Config
quiet bool
}
// NewAdminPlugin creates a new AdminPlugin with the Plugin interface
func New(bot bot.Bot) *AdminPlugin {
func New(b bot.Bot) *AdminPlugin {
p := &AdminPlugin{
Bot: bot,
db: bot.DB(),
bot: b,
db: b.DB(),
cfg: b.Config(),
}
p.LoadData()
b.Register(p, bot.Message, p.message)
b.Register(p, bot.Help, p.help)
p.registerWeb()
return p
}
var forbiddenKeys = map[string]bool{
"twitch.authorization": true,
"twitch.clientid": true,
"untappd.token": true,
"slack.token": true,
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *AdminPlugin) Message(message msg.Message) bool {
func (p *AdminPlugin) message(conn bot.Connector, k bot.Kind, message msg.Message, args ...interface{}) bool {
body := message.Body
if p.quiet {
return true
}
if len(body) > 0 && body[0] == '$' {
return p.handleVariables(message)
return p.handleVariables(conn, message)
}
if !message.Command {
return false
}
if strings.ToLower(body) == "shut up" {
dur := time.Duration(p.cfg.GetInt("quietDuration", 5)) * time.Minute
log.Info().Msgf("Going to sleep for %v, %v", dur, time.Now().Add(dur))
p.bot.Send(conn, bot.Message, message.Channel, "Okay. I'll be back later.")
p.quiet = true
go func() {
select {
case <-time.After(dur):
p.quiet = false
log.Info().Msg("Waking up from nap.")
}
}()
return true
}
if strings.ToLower(body) == "password" {
p.bot.Send(conn, bot.Message, message.Channel, p.bot.GetPassword())
return true
}
parts := strings.Split(body, " ")
if parts[0] == "set" && len(parts) > 2 && forbiddenKeys[parts[1]] {
p.bot.Send(conn, bot.Message, message.Channel, "You cannot access that key")
return true
} else if parts[0] == "set" && len(parts) > 2 {
p.cfg.Set(parts[1], strings.Join(parts[2:], " "))
p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("Set %s", parts[1]))
return true
}
if parts[0] == "get" && len(parts) == 2 && forbiddenKeys[parts[1]] {
p.bot.Send(conn, bot.Message, message.Channel, "You cannot access that key")
return true
} else if parts[0] == "get" && len(parts) == 2 {
v := p.cfg.Get(parts[1], "<unknown>")
p.bot.Send(conn, bot.Message, message.Channel, fmt.Sprintf("%s: %s", parts[1], v))
return true
}
return false
}
func (p *AdminPlugin) handleVariables(message msg.Message) bool {
func (p *AdminPlugin) handleVariables(conn bot.Connector, message msg.Message) bool {
if parts := strings.SplitN(message.Body, "!=", 2); len(parts) == 2 {
variable := strings.ToLower(strings.TrimSpace(parts[0]))
value := strings.TrimSpace(parts[1])
_, err := p.db.Exec(`delete from variables where name=? and value=?`, variable, value)
if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err)
p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Error().Err(err)
} else {
p.Bot.SendMessage(message.Channel, "Removed.")
p.bot.Send(conn, bot.Message, message.Channel, "Removed.")
}
return true
@ -69,50 +135,65 @@ func (p *AdminPlugin) handleVariables(message msg.Message) bool {
row := p.db.QueryRow(`select count(*) from variables where value = ?`, variable, value)
err := row.Scan(&count)
if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err)
p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Error().Err(err)
return true
}
if count > 0 {
p.Bot.SendMessage(message.Channel, "I've already got that one.")
p.bot.Send(conn, bot.Message, message.Channel, "I've already got that one.")
} else {
_, err := p.db.Exec(`INSERT INTO variables (name, value) VALUES (?, ?)`, variable, value)
if err != nil {
p.Bot.SendMessage(message.Channel, "I'm broke and need attention in my variable creation code.")
log.Println("[admin]: ", err)
p.bot.Send(conn, bot.Message, message.Channel, "I'm broke and need attention in my variable creation code.")
log.Error().Err(err)
return true
}
p.Bot.SendMessage(message.Channel, "Added.")
p.bot.Send(conn, bot.Message, message.Channel, "Added.")
}
return true
}
// LoadData imports any configuration data into the plugin. This is not strictly necessary other
// than the fact that the Plugin interface demands it exist. This may be deprecated at a later
// date.
func (p *AdminPlugin) LoadData() {
// This bot has no data to load
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *AdminPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "This does super secret things that you're not allowed to know about.")
func (p *AdminPlugin) help(conn bot.Connector, kind bot.Kind, m msg.Message, args ...interface{}) bool {
p.bot.Send(conn, bot.Message, m.Channel, "This does super secret things that you're not allowed to know about.")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *AdminPlugin) Event(kind string, message msg.Message) bool {
return false
func (p *AdminPlugin) registerWeb() {
http.HandleFunc("/vars/api", p.handleWebAPI)
http.HandleFunc("/vars", p.handleWeb)
p.bot.RegisterWeb("/vars", "Variables")
}
// Handler for bot's own messages
func (p *AdminPlugin) BotMessage(message msg.Message) bool {
return false
var tpl = template.Must(template.New("factoidIndex").Parse(varIndex))
func (p *AdminPlugin) handleWeb(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.bot.GetWebNavigation()})
}
// Register any web URLs desired
func (p *AdminPlugin) RegisterWeb() *string {
return nil
func (p *AdminPlugin) handleWebAPI(w http.ResponseWriter, r *http.Request) {
var configEntries []struct {
Key string `json:"key"`
Value string `json:"value"`
}
q := `select key, value from config`
err := p.db.Select(&configEntries, q)
if err != nil {
log.Error().
Err(err).
Msg("Error getting config entries.")
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
for i, e := range configEntries {
if strings.Contains(e.Value, ";;") {
e.Value = strings.ReplaceAll(e.Value, ";;", ", ")
e.Value = fmt.Sprintf("[%s]", e.Value)
configEntries[i] = e
}
}
j, _ := json.Marshal(configEntries)
fmt.Fprintf(w, "%s", j)
}
func (p *AdminPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

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

View File

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

View File

@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"strconv"
@ -15,6 +14,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/plugins/counter"
@ -37,34 +37,33 @@ type untappdUser struct {
chanNick string
}
// NewBeersPlugin creates a new BeersPlugin with the Plugin interface
func New(bot bot.Bot) *BeersPlugin {
if bot.DBVersion() == 1 {
if _, err := bot.DB().Exec(`create table if not exists untappd (
// New BeersPlugin creates a new BeersPlugin with the Plugin interface
func New(b bot.Bot) *BeersPlugin {
if _, err := b.DB().Exec(`create table if not exists untappd (
id integer primary key,
untappdUser string,
channel string,
lastCheckin integer,
chanNick string
);`); err != nil {
log.Fatal(err)
}
log.Fatal().Err(err)
}
p := BeersPlugin{
Bot: bot,
db: bot.DB(),
p := &BeersPlugin{
Bot: b,
db: b.DB(),
}
p.LoadData()
for _, channel := range bot.Config().Untappd.Channels {
go p.untappdLoop(channel)
for _, channel := range b.Config().GetArray("Untappd.Channels", []string{}) {
go p.untappdLoop(b.DefaultConnector(), channel)
}
return &p
b.Register(p, bot.Message, p.message)
b.Register(p, bot.Help, p.help)
return p
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *BeersPlugin) Message(message msg.Message) bool {
func (p *BeersPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
parts := strings.Fields(message.Body)
if len(parts) == 0 {
@ -84,49 +83,49 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
count, err := strconv.Atoi(parts[2])
if err != nil {
// if it's not a number, maybe it's a nick!
p.Bot.SendMessage(channel, "Sorry, that didn't make any sense.")
p.Bot.Send(c, bot.Message, channel, "Sorry, that didn't make any sense.")
}
if count < 0 {
// you can't be negative
msg := fmt.Sprintf("Sorry %s, you can't have negative beers!", nick)
p.Bot.SendMessage(channel, msg)
p.Bot.Send(c, bot.Message, channel, msg)
return true
}
if parts[1] == "+=" {
p.addBeers(nick, count)
p.randomReply(channel)
p.randomReply(c, channel)
} else if parts[1] == "=" {
if count == 0 {
p.puke(nick, channel)
p.puke(c, nick, channel)
} else {
p.setBeers(nick, count)
p.randomReply(channel)
p.randomReply(c, channel)
}
} else {
p.Bot.SendMessage(channel, "I don't know your math.")
p.Bot.Send(c, bot.Message, channel, "I don't know your math.")
}
} else if len(parts) == 2 {
if p.doIKnow(parts[1]) {
p.reportCount(parts[1], channel, false)
p.reportCount(c, parts[1], channel, false)
} else {
msg := fmt.Sprintf("Sorry, I don't know %s.", parts[1])
p.Bot.SendMessage(channel, msg)
p.Bot.Send(c, bot.Message, channel, msg)
}
} else if len(parts) == 1 {
p.reportCount(nick, channel, true)
p.reportCount(c, nick, channel, true)
}
// no matter what, if we're in here, then we've responded
return true
} else if parts[0] == "puke" {
p.puke(nick, channel)
p.puke(c, nick, channel)
return true
}
if message.Command && parts[0] == "imbibe" {
p.addBeers(nick, 1)
p.randomReply(channel)
p.randomReply(c, channel)
return true
}
@ -135,7 +134,7 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel := message.Channel
if len(parts) < 2 {
p.Bot.SendMessage(channel, "You must also provide a user name.")
p.Bot.Send(c, bot.Message, channel, "You must also provide a user name.")
} else if len(parts) == 3 {
chanNick = parts[2]
} else if len(parts) == 4 {
@ -148,16 +147,19 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
channel: channel,
}
log.Println("Creating Untappd user:", u.untappdUser, "nick:", u.chanNick)
log.Info().
Str("untappdUser", u.untappdUser).
Str("nick", u.chanNick).
Msg("Creating Untappd user")
var count int
err := p.db.QueryRow(`select count(*) from untappd
where untappdUser = ?`, u.untappdUser).Scan(&count)
if err != nil {
log.Println("Error registering untappd: ", err)
log.Error().Err(err).Msgf("Error registering untappd")
}
if count > 0 {
p.Bot.SendMessage(channel, "I'm already watching you.")
p.Bot.Send(c, bot.Message, channel, "I'm already watching you.")
return true
}
_, err = p.db.Exec(`insert into untappd (
@ -172,45 +174,36 @@ func (p *BeersPlugin) Message(message msg.Message) bool {
u.chanNick,
)
if err != nil {
log.Println("Error registering untappd: ", err)
p.Bot.SendMessage(channel, "I can't see.")
log.Error().Err(err).Msgf("Error registering untappd")
p.Bot.Send(c, bot.Message, channel, "I can't see.")
return true
}
p.Bot.SendMessage(channel, "I'll be watching you.")
p.Bot.Send(c, bot.Message, channel, "I'll be watching you.")
p.checkUntappd(channel)
p.checkUntappd(c, channel)
return true
}
if message.Command && parts[0] == "checkuntappd" {
log.Println("Checking untappd at request of user.")
p.checkUntappd(channel)
log.Info().
Str("user", message.User.Name).
Msgf("Checking untappd at request of user.")
p.checkUntappd(c, channel)
return true
}
return false
}
// Empty event handler because this plugin does not do anything on event recv
func (p *BeersPlugin) Event(kind string, message msg.Message) bool {
return false
}
// LoadData imports any configuration data into the plugin. This is not strictly necessary other
// than the fact that the Plugin interface demands it exist. This may be deprecated at a later
// date.
func (p *BeersPlugin) LoadData() {
rand.Seed(time.Now().Unix())
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *BeersPlugin) Help(channel string, parts []string) {
func (p *BeersPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
msg := "Beers: imbibe by using either beers +=,=,++ or with the !imbibe/drink " +
"commands. I'll keep a count of how many beers you've had and then if you want " +
"to reset, just !puke it all up!"
p.Bot.SendMessage(channel, msg)
p.Bot.Send(c, bot.Message, message.Channel, msg)
return true
}
func getUserBeers(db *sqlx.DB, user string) counter.Item {
@ -222,7 +215,7 @@ func (p *BeersPlugin) setBeers(user string, amount int) {
ub := getUserBeers(p.db, user)
err := ub.Update(amount)
if err != nil {
log.Println("Error saving beers: ", err)
log.Error().Err(err).Msgf("Error saving beers")
}
}
@ -230,7 +223,7 @@ func (p *BeersPlugin) addBeers(user string, delta int) {
ub := getUserBeers(p.db, user)
err := ub.UpdateDelta(delta)
if err != nil {
log.Println("Error saving beers: ", err)
log.Error().Err(err).Msgf("Error saving beers")
}
}
@ -239,7 +232,7 @@ func (p *BeersPlugin) getBeers(nick string) int {
return ub.Count
}
func (p *BeersPlugin) reportCount(nick, channel string, himself bool) {
func (p *BeersPlugin) reportCount(c bot.Connector, nick, channel string, himself bool) {
beers := p.getBeers(nick)
msg := fmt.Sprintf("%s has had %d beers so far.", nick, beers)
if himself {
@ -249,13 +242,13 @@ func (p *BeersPlugin) reportCount(nick, channel string, himself bool) {
msg = fmt.Sprintf("You've had %d beers so far, %s.", beers, nick)
}
}
p.Bot.SendMessage(channel, msg)
p.Bot.Send(c, bot.Message, channel, msg)
}
func (p *BeersPlugin) puke(user string, channel string) {
func (p *BeersPlugin) puke(c bot.Connector, user string, channel string) {
p.setBeers(user, 0)
msg := fmt.Sprintf("Ohhhhhh, and a reversal of fortune for %s!", user)
p.Bot.SendMessage(channel, msg)
p.Bot.Send(c, bot.Message, channel, msg)
}
func (p *BeersPlugin) doIKnow(nick string) bool {
@ -268,9 +261,9 @@ func (p *BeersPlugin) doIKnow(nick string) bool {
}
// Sends random affirmation to the channel. This could be better (with a datastore for sayings)
func (p *BeersPlugin) randomReply(channel string) {
func (p *BeersPlugin) randomReply(c bot.Connector, channel string) {
replies := []string{"ZIGGY! ZAGGY!", "HIC!", "Stay thirsty, my friend!"}
p.Bot.SendMessage(channel, replies[rand.Intn(len(replies))])
p.Bot.Send(c, bot.Message, channel, replies[rand.Intn(len(replies))])
}
type checkin struct {
@ -316,7 +309,12 @@ type Beers struct {
}
func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
access_token := "?access_token=" + p.Bot.Config().Untappd.Token
token := p.Bot.Config().Get("Untappd.Token", "NONE")
if token == "NONE" {
return []checkin{}, fmt.Errorf("No untappd token")
}
access_token := "?access_token=" + token
baseUrl := "https://api.untappd.com/v4/checkin/recent/"
url := baseUrl + access_token + "&limit=25"
@ -332,55 +330,54 @@ func (p *BeersPlugin) pullUntappd() ([]checkin, error) {
}
if resp.StatusCode == 500 {
log.Printf("Error querying untappd: %s, %s", resp.Status, body)
log.Error().Msgf("Error querying untappd: %s, %s", resp.Status, body)
return []checkin{}, errors.New(resp.Status)
}
var beers Beers
err = json.Unmarshal(body, &beers)
if err != nil {
log.Println(err)
log.Error().Err(err)
return []checkin{}, err
}
return beers.Response.Checkins.Items, nil
}
func (p *BeersPlugin) checkUntappd(channel string) {
token := p.Bot.Config().Untappd.Token
if token == "" || token == "<Your Token>" {
log.Println("No Untappd token, cannot enable plugin.")
func (p *BeersPlugin) checkUntappd(c bot.Connector, channel string) {
token := p.Bot.Config().Get("Untappd.Token", "NONE")
if token == "NONE" {
log.Info().
Msg(`Set config value "untappd.token" if you wish to enable untappd`)
return
}
userMap := make(map[string]untappdUser)
rows, err := p.db.Query(`select id, untappdUser, channel, lastCheckin, chanNick from untappd;`)
if err != nil {
log.Println("Error getting untappd users: ", err)
log.Error().Err(err).Msg("Error getting untappd users")
return
}
for rows.Next() {
u := untappdUser{}
err := rows.Scan(&u.id, &u.untappdUser, &u.channel, &u.lastCheckin, &u.chanNick)
if err != nil {
log.Fatal(err)
log.Fatal().Err(err)
}
userMap[u.untappdUser] = u
log.Printf("Found untappd user: %#v", u)
if u.chanNick == "" {
log.Fatal("Empty chanNick for no good reason.")
log.Fatal().Msg("Empty chanNick for no good reason.")
}
}
chks, err := p.pullUntappd()
if err != nil {
log.Println("Untappd ERROR: ", err)
log.Error().Err(err).Msg("Untappd ERROR")
return
}
for i := len(chks); i > 0; i-- {
checkin := chks[i-1]
if checkin.Checkin_id <= userMap[checkin.User.User_name].lastCheckin {
log.Printf("User %s already check in >%d", checkin.User.User_name, checkin.Checkin_id)
continue
}
@ -395,8 +392,9 @@ func (p *BeersPlugin) checkUntappd(channel string) {
if !ok {
continue
}
log.Printf("user.chanNick: %s, user.untappdUser: %s, checkin.User.User_name: %s",
user.chanNick, user.untappdUser, checkin.User.User_name)
log.Debug().
Msgf("user.chanNick: %s, user.untappdUser: %s, checkin.User.User_name: %s",
user.chanNick, user.untappdUser, checkin.User.User_name)
p.addBeers(user.chanNick, 1)
drunken := p.getBeers(user.chanNick)
@ -410,11 +408,18 @@ func (p *BeersPlugin) checkUntappd(channel string) {
msg, checkin.Checkin_comment)
}
args := []interface{}{
channel,
msg,
}
if checkin.Media.Count > 0 {
if strings.Contains(checkin.Media.Items[0].Photo.Photo_img_lg, "photos-processing") {
continue
}
msg += "\nHere's a photo: " + checkin.Media.Items[0].Photo.Photo_img_lg
args = append(args, bot.ImageAttachment{
URL: checkin.Media.Items[0].Photo.Photo_img_lg,
AltTxt: "Here's a photo",
})
}
user.lastCheckin = checkin.Checkin_id
@ -422,33 +427,27 @@ func (p *BeersPlugin) checkUntappd(channel string) {
lastCheckin = ?
where id = ?`, user.lastCheckin, user.id)
if err != nil {
log.Println("UPDATE ERROR!:", err)
log.Error().Err(err).Msg("UPDATE ERROR!")
}
log.Println("checkin id:", checkin.Checkin_id, "Message:", msg)
p.Bot.SendMessage(channel, msg)
log.Debug().
Int("checkin_id", checkin.Checkin_id).
Str("msg", msg).
Msg("checkin")
p.Bot.Send(c, bot.Message, args...)
}
}
func (p *BeersPlugin) untappdLoop(channel string) {
frequency := p.Bot.Config().Untappd.Freq
func (p *BeersPlugin) untappdLoop(c bot.Connector, channel string) {
frequency := p.Bot.Config().GetInt("Untappd.Freq", 120)
if frequency == 0 {
return
}
log.Println("Checking every ", frequency, " seconds")
log.Info().Msgf("Checking every %v seconds", frequency)
for {
time.Sleep(time.Duration(frequency) * time.Second)
p.checkUntappd(channel)
p.checkUntappd(c, channel)
}
}
// Handler for bot's own messages
func (p *BeersPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *BeersPlugin) RegisterWeb() *string {
return nil
}
func (p *BeersPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

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
}
func New(bot bot.Bot) *CSWPlugin {
return &CSWPlugin{
Bot: bot,
func New(b bot.Bot) *CSWPlugin {
csw := &CSWPlugin{
Bot: b,
}
b.Register(csw, bot.Message, csw.message)
return csw
}
func (p *CSWPlugin) Message(message msg.Message) bool {
func (p *CSWPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@ -63,25 +65,9 @@ func (p *CSWPlugin) Message(message msg.Message) bool {
}
}
p.Bot.SendMessage(message.Channel, responses[rand.Intn(len(responses))])
p.Bot.Send(c, bot.Message, message.Channel, responses[rand.Intn(len(responses))])
return true
}
return false
}
func (p *CSWPlugin) Help(channel string, parts []string) {}
func (p *CSWPlugin) Event(kind string, message msg.Message) bool {
return false
}
func (p *CSWPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *CSWPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
func (p *CSWPlugin) RegisterWeb() *string {
return nil
}

View File

@ -3,6 +3,7 @@
package couldashouldawoulda
import (
"github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
func makeMessage(payload string) msg.Message {
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@ -29,7 +30,7 @@ func Test0(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!should I drink a beer?"))
res := c.message(makeMessage("!should I drink a beer?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -47,7 +48,7 @@ func Test1(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!should I drink a beer or a bourbon?"))
res := c.message(makeMessage("!should I drink a beer or a bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"}
@ -65,7 +66,7 @@ func Test2(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!could I drink a beer or a bourbon?"))
res := c.message(makeMessage("!could I drink a beer or a bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -83,7 +84,7 @@ func Test3(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!would I die if I drank too much bourbon?"))
res := c.message(makeMessage("!would I die if I drank too much bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"Yes.", "No.", "Maybe.", "For fucks sake, how should I know?"}
@ -101,7 +102,7 @@ func Test4(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!would I die or be sick if I drank all the bourbon?"))
res := c.message(makeMessage("!would I die or be sick if I drank all the bourbon?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"The former.", "The latter.", "Obviously the former.", "Clearly the latter.", "Can't it be both?"}
@ -119,7 +120,7 @@ func Test5(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!should I have another beer or bourbon or tequila?"))
res := c.message(makeMessage("!should I have another beer or bourbon or tequila?"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
possibilities := []string{"I'd say option", "You'd be an idiot not to choose the"}

View File

@ -1,15 +1,18 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package counter
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"html/template"
"math/rand"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/jmoiron/sqlx"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
@ -41,6 +44,20 @@ type alias struct {
PointsTo string `db:"points_to"`
}
// GetItems returns all counters
func GetAllItems(db *sqlx.DB) ([]Item, error) {
var items []Item
err := db.Select(&items, `select * from counter`)
if err != nil {
return nil, err
}
// Don't forget to embed the DB into all of that shiz
for i := range items {
items[i].DB = db
}
return items, nil
}
// GetItems returns all counters for a subject
func GetItems(db *sqlx.DB, nick string) ([]Item, error) {
var items []Item
@ -111,7 +128,7 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil {
itemName = a.PointsTo
} else {
log.Println(err, a)
log.Error().Err(err).Interface("alias", a)
}
err := db.Get(&item, `select * from counter where nick = ? and item= ?`,
@ -125,7 +142,11 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
default:
return Item{}, err
}
log.Printf("Got item %s.%s: %#v", nick, itemName, item)
log.Debug().
Str("nick", nick).
Str("itemName", itemName).
Interface("item", item).
Msg("got item")
return item, nil
}
@ -133,6 +154,9 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
func (i *Item) Create() error {
res, err := i.Exec(`insert into counter (nick, item, count) values (?, ?, ?);`,
i.Nick, i.Item, i.Count)
if err != nil {
return err
}
id, _ := res.LastInsertId()
// hackhackhack?
i.ID = id
@ -149,7 +173,10 @@ func (i *Item) Update(value int) error {
if i.ID == -1 {
i.Create()
}
log.Printf("Updating item: %#v, value: %d", i, value)
log.Debug().
Interface("i", i).
Int("value", value).
Msg("Updating item")
_, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID)
return err
}
@ -169,33 +196,35 @@ func (i *Item) Delete() error {
}
// NewCounterPlugin creates a new CounterPlugin with the Plugin interface
func New(bot bot.Bot) *CounterPlugin {
if _, err := bot.DB().Exec(`create table if not exists counter (
func New(b bot.Bot) *CounterPlugin {
tx := b.DB().MustBegin()
b.DB().MustExec(`create table if not exists counter (
id integer primary key,
nick string,
item string,
count integer
);`); err != nil {
log.Fatal(err)
}
if _, err := bot.DB().Exec(`create table if not exists counter_alias (
);`)
b.DB().MustExec(`create table if not exists counter_alias (
id integer PRIMARY KEY AUTOINCREMENT,
item string NOT NULL UNIQUE,
points_to string NOT NULL
);`); err != nil {
log.Fatal(err)
}
return &CounterPlugin{
Bot: bot,
DB: bot.DB(),
);`)
tx.Commit()
cp := &CounterPlugin{
Bot: b,
DB: b.DB(),
}
b.Register(cp, bot.Message, cp.message)
b.Register(cp, bot.Help, cp.help)
cp.registerWeb()
return cp
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the
// users message. Otherwise, the function returns false and the bot continues
// execution of other plugins.
func (p *CounterPlugin) Message(message msg.Message) bool {
func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
// This bot does not reply to anything
nick := message.User.Name
channel := message.Channel
@ -207,10 +236,10 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
if len(parts) == 3 && strings.ToLower(parts[0]) == "mkalias" {
if _, err := MkAlias(p.DB, parts[1], parts[2]); err != nil {
log.Println(err)
log.Error().Err(err)
return false
}
p.Bot.SendMessage(channel, fmt.Sprintf("Created alias %s -> %s",
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("Created alias %s -> %s",
parts[1], parts[2]))
return true
} else if strings.ToLower(parts[0]) == "leaderboard" {
@ -226,7 +255,7 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
its, err := cmd()
if err != nil {
log.Println(err)
log.Error().Err(err)
return false
} else if len(its) == 0 {
return false
@ -240,23 +269,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it.Item,
)
}
p.Bot.SendMessage(channel, out)
p.Bot.Send(c, bot.Message, channel, out)
return true
} else if match := teaMatcher.MatchString(message.Body); match {
// check for tea match TTT
return p.checkMatch(message)
return p.checkMatch(c, message)
} else if message.Command && message.Body == "reset me" {
items, err := GetItems(p.DB, strings.ToLower(nick))
if err != nil {
log.Printf("Error getting items to reset %s: %s", nick, err)
p.Bot.SendMessage(channel, "Something is technically wrong with your counters.")
log.Error().
Err(err).
Str("nick", nick).
Msg("Error getting items to reset")
p.Bot.Send(c, bot.Message, channel, "Something is technically wrong with your counters.")
return true
}
log.Printf("Items: %+v", items)
log.Debug().Msgf("Items: %+v", items)
for _, item := range items {
item.Delete()
}
p.Bot.SendMessage(channel, fmt.Sprintf("%s, you are as new, my son.", nick))
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s, you are as new, my son.", nick))
return true
} else if message.Command && parts[0] == "inspect" && len(parts) == 2 {
var subject string
@ -267,12 +299,17 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
subject = strings.ToLower(parts[1])
}
log.Printf("Getting counter for %s", subject)
log.Debug().
Str("subject", subject).
Msg("Getting counter")
// pull all of the items associated with "subject"
items, err := GetItems(p.DB, subject)
if err != nil {
log.Fatalf("Error retrieving items for %s: %s", subject, err)
p.Bot.SendMessage(channel, "Something went wrong finding that counter;")
log.Error().
Err(err).
Str("subject", subject).
Msg("Error retrieving items")
p.Bot.Send(c, bot.Message, channel, "Something went wrong finding that counter;")
return true
}
@ -292,11 +329,11 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
resp += "."
if count == 0 {
p.Bot.SendMessage(channel, fmt.Sprintf("%s has no counters.", subject))
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has no counters.", subject))
return true
}
p.Bot.SendMessage(channel, resp)
p.Bot.Send(c, bot.Message, channel, resp)
return true
} else if message.Command && len(parts) == 2 && parts[0] == "clear" {
subject := strings.ToLower(nick)
@ -304,18 +341,26 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
it, err := GetItem(p.DB, subject, itemName)
if err != nil {
log.Printf("Error getting item to remove %s.%s: %s", subject, itemName, err)
p.Bot.SendMessage(channel, "Something went wrong removing that counter;")
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error getting item to remove")
p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
return true
}
err = it.Delete()
if err != nil {
log.Printf("Error removing item %s.%s: %s", subject, itemName, err)
p.Bot.SendMessage(channel, "Something went wrong removing that counter;")
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error removing item")
p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
return true
}
p.Bot.SendAction(channel, fmt.Sprintf("chops a few %s out of his brain",
p.Bot.Send(c, bot.Action, channel, fmt.Sprintf("chops a few %s out of his brain",
itemName))
return true
@ -338,16 +383,19 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
item, err := GetItem(p.DB, subject, itemName)
switch {
case err == sql.ErrNoRows:
p.Bot.SendMessage(channel, fmt.Sprintf("I don't think %s has any %s.",
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("I don't think %s has any %s.",
subject, itemName))
return true
case err != nil:
log.Printf("Error retrieving item count for %s.%s: %s",
subject, itemName, err)
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error retrieving item count")
return true
}
p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject, item.Count,
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject, item.Count,
itemName))
return true
@ -372,25 +420,33 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// ++ those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("error finding item")
// Item ain't there, I guess
return false
}
log.Printf("About to update item: %#v", item)
log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1)
p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
} else if strings.HasSuffix(parts[0], "--") {
// -- those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
// Item ain't there, I guess
return false
}
item.UpdateDelta(-1)
p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
}
@ -412,28 +468,36 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
// += those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
// Item ain't there, I guess
return false
}
n, _ := strconv.Atoi(parts[2])
log.Printf("About to update item by %d: %#v", n, item)
log.Debug().Msgf("About to update item by %d: %#v", n, item)
item.UpdateDelta(n)
p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
} else if parts[1] == "-=" {
// -= those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
log.Printf("Error finding item %s.%s: %s.", subject, itemName, err)
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
// Item ain't there, I guess
return false
}
n, _ := strconv.Atoi(parts[2])
log.Printf("About to update item by -%d: %#v", n, item)
log.Debug().Msgf("About to update item by -%d: %#v", n, item)
item.UpdateDelta(-n)
p.Bot.SendMessage(channel, fmt.Sprintf("%s has %d %s.", subject,
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
item.Count, item.Item))
return true
}
@ -443,31 +507,15 @@ func (p *CounterPlugin) Message(message msg.Message) bool {
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *CounterPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "You can set counters incrementally by using "+
func (p *CounterPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.Send(c, bot.Message, message.Channel, "You can set counters incrementally by using "+
"<noun>++ and <noun>--. You can see all of your counters using "+
"\"inspect\", erase them with \"clear\", and view single counters with "+
"\"count\".")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *CounterPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *CounterPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *CounterPlugin) RegisterWeb() *string {
return nil
}
func (p *CounterPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }
func (p *CounterPlugin) checkMatch(message msg.Message) bool {
func (p *CounterPlugin) checkMatch(c bot.Connector, message msg.Message) bool {
nick := message.User.Name
channel := message.Channel
@ -480,13 +528,98 @@ func (p *CounterPlugin) checkMatch(message msg.Message) bool {
// We will specifically allow :tea: to keep compatability
item, err := GetItem(p.DB, nick, itemName)
if err != nil || (item.Count == 0 && item.Item != ":tea:") {
log.Printf("Error finding item %s.%s: %s.", nick, itemName, err)
log.Error().
Err(err).
Str("itemName", itemName).
Msg("Error finding item")
// Item ain't there, I guess
return false
}
log.Printf("About to update item: %#v", item)
log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1)
p.Bot.SendMessage(channel, fmt.Sprintf("bleep-bloop-blop... %s has %d %s",
nick, item.Count, itemName))
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s... %s has %d %s",
strings.Join(everyDayImShuffling([]string{"bleep", "bloop", "blop"}), "-"), nick, item.Count, itemName))
return true
}
func everyDayImShuffling(vals []string) []string {
ret := make([]string, len(vals))
perm := rand.Perm(len(vals))
for i, randIndex := range perm {
ret[i] = vals[randIndex]
}
return ret
}
func (p *CounterPlugin) registerWeb() {
http.HandleFunc("/counter/api", p.handleCounterAPI)
http.HandleFunc("/counter", p.handleCounter)
p.Bot.RegisterWeb("/counter", "Counter")
}
var tpl = template.Must(template.New("factoidIndex").Parse(html))
func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.Bot.GetWebNavigation()})
}
func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
info := struct {
User string
Thing string
Action string
Password string
}{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&info)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
log.Debug().
Interface("postbody", info).
Msg("Got a POST")
if info.Password != p.Bot.GetPassword() {
w.WriteHeader(http.StatusForbidden)
j, _ := json.Marshal(struct{ Err string }{Err: "Invalid Password"})
w.Write(j)
return
}
item, err := GetItem(p.DB, info.User, info.Thing)
if err != nil {
log.Error().
Err(err).
Str("subject", info.User).
Str("itemName", info.Thing).
Msg("error finding item")
w.WriteHeader(404)
fmt.Fprint(w, err)
return
}
if info.Action == "++" {
item.UpdateDelta(1)
} else if info.Action == "--" {
item.UpdateDelta(-1)
} else {
w.WriteHeader(400)
fmt.Fprint(w, "Invalid increment")
return
}
}
all, err := GetAllItems(p.DB)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
data, err := json.Marshal(all)
if err != nil {
w.WriteHeader(500)
fmt.Fprint(w, err)
return
}
fmt.Fprint(w, string(data))
}

View File

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

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
func New(bot bot.Bot) *DicePlugin {
return &DicePlugin{
Bot: bot,
func New(b bot.Bot) *DicePlugin {
dp := &DicePlugin{
Bot: b,
}
b.Register(dp, bot.Message, dp.message)
b.Register(dp, bot.Help, dp.help)
return dp
}
func rollDie(sides int) int {
@ -32,7 +35,7 @@ func rollDie(sides int) int {
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *DicePlugin) Message(message msg.Message) bool {
func (p *DicePlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@ -46,7 +49,7 @@ func (p *DicePlugin) Message(message msg.Message) bool {
}
if sides < 2 || nDice < 1 || nDice > 20 {
p.Bot.SendMessage(channel, "You're a dick.")
p.Bot.Send(c, bot.Message, channel, "You're a dick.")
return true
}
@ -61,29 +64,13 @@ func (p *DicePlugin) Message(message msg.Message) bool {
}
}
p.Bot.SendMessage(channel, rolls)
p.Bot.Send(c, bot.Message, channel, rolls)
return true
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *DicePlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Roll dice using notation XdY. Try \"3d20\".")
func (p *DicePlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.Send(c, bot.Message, message.Channel, "Roll dice using notation XdY. Try \"3d20\".")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *DicePlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *DicePlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *DicePlugin) RegisterWeb() *string {
return nil
}
func (p *DicePlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

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

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

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?
var factoidIndex string = `
var factoidIndex = `
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<title>Factoids</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pure/0.6.0/base-min.css">
<!-- 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" />
<!-- DataTables CSS -->
<link rel="stylesheet" type="text/css" href="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css">
<!-- 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 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>
<div>
<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>
<body>
<div>
<style scoped>
.pure-button-success,
.pure-button-error,
.pure-button-warning,
.pure-button-secondary {
color: white;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
padding: 2px;
}
.pure-button-success {
background: rgb(76, 201, 71); /* this is a green */
}
.pure-button-error {
background: rgb(202, 60, 60); /* this is a maroon */
}
.pure-button-warning {
background: orange;
}
.pure-button-secondary {
background: rgb(95, 198, 218); /* this is a light blue */
}
</style>
{{if .Error}}
<span id="error" class="pure-button-error">{{.Error}}</span>
{{end}}
{{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>
<div id="app">
<b-navbar>
<b-navbar-brand>Factoids</b-navbar-brand>
<b-navbar-nav>
<b-nav-item v-for="item in nav" :href="item.URL" :active="item.Name === 'Factoid'">{{ "{{ item.Name }}" }}</b-nav-item>
</b-navbar-nav>
</b-navbar>
<b-alert
dismissable
variant="error"
v-if="err"
@dismissed="err = ''">
{{ "{{ err }}" }}
</b-alert>
<b-form @submit="runQuery">
<b-container>
<b-row>
<b-col cols="10">
<b-input v-model="query"></b-input>
</b-col>
<b-col cols="2">
<b-button>Search</b-button>
</b-col>
</b-row>
<b-row>
<b-col>
<b-table
fixed
:items="results"
:fields="fields"></b-table>
</b-col>
</b-row>
</b-container>
</b-form>
</div>
<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>
`

View File

@ -5,12 +5,12 @@ package first
import (
"database/sql"
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@ -18,26 +18,27 @@ import (
// This is a first plugin to serve as an example and quick copy/paste for new plugins.
type FirstPlugin struct {
First *FirstEntry
Bot bot.Bot
db *sqlx.DB
Bot bot.Bot
db *sqlx.DB
}
type FirstEntry struct {
id int64
day time.Time
time time.Time
body string
nick string
saved bool
id int64
day time.Time
time time.Time
channel string
body string
nick string
saved bool
}
// Insert or update the first entry
func (fe *FirstEntry) save(db *sqlx.DB) error {
if _, err := db.Exec(`insert into first (day, time, body, nick)
values (?, ?, ?, ?)`,
if _, err := db.Exec(`insert into first (day, time, channel, body, nick)
values (?, ?, ?, ?, ?)`,
fe.day.Unix(),
fe.time.Unix(),
fe.channel,
fe.body,
fe.nick,
); err != nil {
@ -48,34 +49,33 @@ func (fe *FirstEntry) save(db *sqlx.DB) error {
// NewFirstPlugin creates a new FirstPlugin with the Plugin interface
func New(b bot.Bot) *FirstPlugin {
if b.DBVersion() == 1 {
_, err := b.DB().Exec(`create table if not exists first (
_, err := b.DB().Exec(`create table if not exists first (
id integer primary key,
day integer,
time integer,
channel string,
body string,
nick string
);`)
if err != nil {
log.Fatal("Could not create first table: ", err)
}
}
log.Println("First plugin initialized with day:", midnight(time.Now()))
first, err := getLastFirst(b.DB())
if err != nil {
log.Fatal("Could not initialize first plugin: ", err)
log.Fatal().
Err(err).
Msg("Could not create first table")
}
return &FirstPlugin{
Bot: b,
db: b.DB(),
First: first,
log.Info().Msgf("First plugin initialized with day: %s",
midnight(time.Now()))
fp := &FirstPlugin{
Bot: b,
db: b.DB(),
}
b.Register(fp, bot.Message, fp.message)
b.Register(fp, bot.Help, fp.help)
return fp
}
func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
func getLastFirst(db *sqlx.DB, channel string) (*FirstEntry, error) {
// Get last first entry
var id sql.NullInt64
var day sql.NullInt64
@ -85,8 +85,9 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
err := db.QueryRow(`select
id, max(day), time, body, nick from first
where channel = ?
limit 1;
`).Scan(
`, channel).Scan(
&id,
&day,
&timeEntered,
@ -95,20 +96,22 @@ func getLastFirst(db *sqlx.DB) (*FirstEntry, error) {
)
switch {
case err == sql.ErrNoRows || !id.Valid:
log.Println("No previous first entries")
log.Info().Msg("No previous first entries")
return nil, nil
case err != nil:
log.Println("Error on first query row: ", err)
log.Warn().Err(err).Msg("Error on first query row")
return nil, err
}
log.Println(id, day, timeEntered, body, nick)
log.Debug().Msgf("id: %v day %v time %v body %v nick %v",
id, day, timeEntered, body, nick)
return &FirstEntry{
id: id.Int64,
day: time.Unix(day.Int64, 0),
time: time.Unix(timeEntered.Int64, 0),
body: body.String,
nick: nick.String,
saved: true,
id: id.Int64,
day: time.Unix(day.Int64, 0),
time: time.Unix(timeEntered.Int64, 0),
channel: channel,
body: body.String,
nick: nick.String,
saved: true,
}, nil
}
@ -117,7 +120,11 @@ func midnight(t time.Time) time.Time {
return time.Date(y, m, d, 0, 0, 0, 0, time.UTC)
}
func isToday(t time.Time) bool {
func isNotToday(f *FirstEntry) bool {
if f == nil {
return true
}
t := f.time
t0 := midnight(t)
return t0.Before(midnight(time.Now()))
}
@ -125,26 +132,42 @@ func isToday(t time.Time) bool {
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *FirstPlugin) Message(message msg.Message) bool {
// This bot does not reply to anything
func (p *FirstPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
log.Debug().
Interface("msg", message).
Msg("First is looking at a message")
if p.First == nil && p.allowed(message) {
log.Printf("No previous first. Recording new first: %s", message.Body)
p.recordFirst(message)
if message.IsIM {
log.Debug().Msg("Skipping IM")
return false
} else if p.First != nil {
if isToday(p.First.time) && p.allowed(message) {
log.Printf("Recording first: %s - %v vs %v", message.Body, p.First.time, time.Now())
p.recordFirst(message)
return false
}
}
r := strings.NewReplacer("'", "", "\"", "", ",", "", ".", "", ":", "",
first, err := getLastFirst(p.db, message.Channel)
if err != nil {
log.Error().
Err(err).
Msg("Error getting last first")
}
log.Debug().Bool("first == nil", first == nil).Msg("Is first nil?")
log.Debug().Bool("first == nil || isNotToday()", isNotToday(first)).Msg("Is it today?")
log.Debug().Bool("p.allowed", p.allowed(message)).Msg("Allowed?")
if (first == nil || isNotToday(first)) && p.allowed(message) {
log.Debug().
Str("body", message.Body).
Interface("t0", first).
Time("t1", time.Now()).
Msg("Recording first")
p.recordFirst(c, message)
return false
}
r := strings.NewReplacer("", "", "'", "", "\"", "", ",", "", ".", "", ":", "",
"?", "", "!", "")
msg := strings.ToLower(message.Body)
if r.Replace(msg) == "whos on first" {
p.announceFirst(message)
m := strings.ToLower(message.Body)
if r.Replace(m) == "whos on first" && first != nil {
p.announceFirst(c, first)
return true
}
@ -152,81 +175,70 @@ func (p *FirstPlugin) Message(message msg.Message) bool {
}
func (p *FirstPlugin) allowed(message msg.Message) bool {
for _, msg := range p.Bot.Config().Bad.Msgs {
match, err := regexp.MatchString(msg, strings.ToLower(message.Body))
for _, m := range p.Bot.Config().GetArray("Bad.Msgs", []string{}) {
match, err := regexp.MatchString(m, strings.ToLower(message.Body))
if err != nil {
log.Println("Bad regexp: ", err)
log.Error().Err(err).Msg("Bad regexp")
}
if match {
log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
log.Info().
Str("user", message.User.Name).
Str("body", message.Body).
Msg("Disallowing first")
return false
}
}
for _, host := range p.Bot.Config().Bad.Hosts {
for _, host := range p.Bot.Config().GetArray("Bad.Hosts", []string{}) {
if host == message.Host {
log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
log.Info().
Str("user", message.User.Name).
Str("body", message.Body).
Msg("Disallowing first")
return false
}
}
for _, nick := range p.Bot.Config().Bad.Nicks {
for _, nick := range p.Bot.Config().GetArray("Bad.Nicks", []string{}) {
if nick == message.User.Name {
log.Println("Disallowing first: ", message.User.Name, ":", message.Body)
log.Info().
Str("user", message.User.Name).
Str("body", message.Body).
Msg("Disallowing first")
return false
}
}
return true
}
func (p *FirstPlugin) recordFirst(message msg.Message) {
log.Println("Recording first: ", message.User.Name, ":", message.Body)
p.First = &FirstEntry{
day: midnight(time.Now()),
time: message.Time,
body: message.Body,
nick: message.User.Name,
func (p *FirstPlugin) recordFirst(c bot.Connector, message msg.Message) {
log.Info().
Str("channel", message.Channel).
Str("user", message.User.Name).
Str("body", message.Body).
Msg("Recording first")
first := &FirstEntry{
day: midnight(time.Now()),
time: message.Time,
channel: message.Channel,
body: message.Body,
nick: message.User.Name,
}
log.Printf("recordFirst: %+v", p.First.day)
err := p.First.save(p.db)
log.Info().Msgf("recordFirst: %+v", first.day)
err := first.save(p.db)
if err != nil {
log.Println("Error saving first entry: ", err)
log.Error().Err(err).Msg("Error saving first entry")
return
}
p.announceFirst(message)
p.announceFirst(c, first)
}
func (p *FirstPlugin) announceFirst(message msg.Message) {
c := message.Channel
if p.First != nil {
p.Bot.SendMessage(c, fmt.Sprintf("%s had first at %s with the message: \"%s\"",
p.First.nick, p.First.time.Format("15:04"), p.First.body))
}
}
// LoadData imports any configuration data into the plugin. This is not strictly necessary other
// than the fact that the Plugin interface demands it exist. This may be deprecated at a later
// date.
func (p *FirstPlugin) LoadData() {
// This bot has no data to load
func (p *FirstPlugin) announceFirst(c bot.Connector, first *FirstEntry) {
ch := first.channel
p.Bot.Send(c, bot.Message, ch, fmt.Sprintf("%s had first at %s with the message: \"%s\"",
first.nick, first.time.Format("15:04"), first.body))
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *FirstPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Sorry, First does not do a goddamn thing.")
func (p *FirstPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.Bot.Send(c, bot.Message, message.Channel, "You can ask 'who's on first?' to find out.")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *FirstPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *FirstPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *FirstPlugin) RegisterWeb() *string {
return nil
}
func (p *FirstPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

@ -20,19 +20,20 @@ type LeftpadPlugin struct {
}
// New creates a new LeftpadPlugin with the Plugin interface
func New(bot bot.Bot) *LeftpadPlugin {
p := LeftpadPlugin{
bot: bot,
config: bot.Config(),
func New(b bot.Bot) *LeftpadPlugin {
p := &LeftpadPlugin{
bot: b,
config: b.Config(),
}
return &p
b.Register(p, bot.Message, p.message)
return p
}
type leftpadResp struct {
Str string
}
func (p *LeftpadPlugin) Message(message msg.Message) bool {
func (p *LeftpadPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !message.Command {
return false
}
@ -42,39 +43,22 @@ func (p *LeftpadPlugin) Message(message msg.Message) bool {
padchar := parts[1]
length, err := strconv.Atoi(parts[2])
if err != nil {
p.bot.SendMessage(message.Channel, "Invalid padding number")
p.bot.Send(c, bot.Message, message.Channel, "Invalid padding number")
return true
}
if length > p.config.LeftPad.MaxLen && p.config.LeftPad.MaxLen > 0 {
msg := fmt.Sprintf("%s would kill me if I did that.", p.config.LeftPad.Who)
p.bot.SendMessage(message.Channel, msg)
maxLen, who := p.config.GetInt("LeftPad.MaxLen", 50), p.config.Get("LeftPad.Who", "Putin")
if length > maxLen && maxLen > 0 {
msg := fmt.Sprintf("%s would kill me if I did that.", who)
p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
text := strings.Join(parts[3:], " ")
res := leftpad.LeftPad(text, length, padchar)
p.bot.SendMessage(message.Channel, res)
p.bot.Send(c, bot.Message, message.Channel, res)
return true
}
return false
}
func (p *LeftpadPlugin) Event(e string, message msg.Message) bool {
return false
}
func (p *LeftpadPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *LeftpadPlugin) Help(e string, m []string) {
}
func (p *LeftpadPlugin) RegisterWeb() *string {
// nothing to register
return nil
}
func (p *LeftpadPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

@ -27,29 +27,32 @@ type NerdepediaPlugin struct {
}
// NewNerdepediaPlugin creates a new NerdepediaPlugin with the Plugin interface
func New(bot bot.Bot) *NerdepediaPlugin {
return &NerdepediaPlugin{
bot: bot,
config: bot.Config(),
func New(b bot.Bot) *NerdepediaPlugin {
np := &NerdepediaPlugin{
bot: b,
config: b.Config(),
}
b.Register(np, bot.Message, np.message)
b.Register(np, bot.Help, np.help)
return np
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *NerdepediaPlugin) Message(message msg.Message) bool {
func (p *NerdepediaPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
lowerCase := strings.ToLower(message.Body)
query := ""
if lowerCase == "may the force be with you" || lowerCase == "help me obi-wan" {
query = "http://starwars.wikia.com/wiki/Special:Random"
} else if lowerCase == "beam me up scotty" || lowerCase == "live long and prosper" {
query = "http://memory-alpha.wikia.com/wiki/Special:Random"
} else if lowerCase == "bless the maker" || lowerCase == "i must not fear" {
} else if lowerCase == "bless the maker" || lowerCase == "i must not fear" || lowerCase == "the spice must flow" {
query = "http://dune.wikia.com/wiki/Special:Random"
} else if lowerCase == "my precious" || lowerCase == "one ring to rule them all" || lowerCase == "one does not simply walk into mordor" {
query = "http://lotr.wikia.com/wiki/Special:Random"
} else if lowerCase == "gotta catch em all" {
query = "https://bulbapedia.bulbagarden.net/wiki/Special:Random"
} else if lowerCase == "pikachu i choose you" || lowerCase == "gotta catch em all" {
query = "http://pokemon.wikia.com/wiki/Special:Random"
}
if query != "" {
@ -78,7 +81,7 @@ func (p *NerdepediaPlugin) Message(message msg.Message) bool {
}
if description != "" && link != "" {
p.bot.SendMessage(message.Channel, fmt.Sprintf("%s (%s)", description, link))
p.bot.Send(c, bot.Message, message.Channel, fmt.Sprintf("%s (%s)", description, link))
return true
}
}
@ -87,23 +90,7 @@ func (p *NerdepediaPlugin) Message(message msg.Message) bool {
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *NerdepediaPlugin) Help(channel string, parts []string) {
p.bot.SendMessage(channel, "nerd stuff")
func (p *NerdepediaPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "nerd stuff")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *NerdepediaPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *NerdepediaPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *NerdepediaPlugin) RegisterWeb() *string {
return nil
}
func (p *NerdepediaPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -3,6 +3,7 @@
package nerdepedia
import (
"github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
func makeMessage(payload string) msg.Message {
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@ -25,11 +26,38 @@ func makeMessage(payload string) msg.Message {
}
}
func TestObiWan(t *testing.T) {
func TestWars(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("help me obi-wan"))
res := c.message(makeMessage("help me obi-wan"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}
func TestTrek(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.message(makeMessage("live long and prosper"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}
func TestDune(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.message(makeMessage("bless the maker"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}
func TestPoke(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.message(makeMessage("gotta catch em all"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
}

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 {
Bot bot.Bot
bot bot.Bot
}
// NewPickerPlugin creates a new PickerPlugin with the Plugin interface
func New(bot bot.Bot) *PickerPlugin {
return &PickerPlugin{
Bot: bot,
func New(b bot.Bot) *PickerPlugin {
pp := &PickerPlugin{
bot: b,
}
b.Register(pp, bot.Message, pp.message)
b.Register(pp, bot.Help, pp.help)
return pp
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *PickerPlugin) Message(message msg.Message) bool {
func (p *PickerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if !strings.HasPrefix(message.Body, "pick") {
return false
}
n, items, err := p.parse(message.Body)
if err != nil {
p.Bot.SendMessage(message.Channel, err.Error())
p.bot.Send(c, bot.Message, message.Channel, err.Error())
return true
}
if n == 1 {
item := items[rand.Intn(len(items))]
out := fmt.Sprintf("I've chosen %q for you.", strings.TrimSpace(item))
p.Bot.SendMessage(message.Channel, out)
p.bot.Send(c, bot.Message, message.Channel, out)
return true
}
@ -59,7 +62,7 @@ func (p *PickerPlugin) Message(message msg.Message) bool {
fmt.Fprintf(&b, ", %q", item)
}
b.WriteString(" }")
p.Bot.SendMessage(message.Channel, b.String())
p.bot.Send(c, bot.Message, message.Channel, b.String())
return true
}
@ -108,23 +111,7 @@ func (p *PickerPlugin) parse(body string) (int, []string, error) {
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *PickerPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Choose from a list of options. Try \"pick {a,b,c}\".")
func (p *PickerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "Choose from a list of options. Try \"pick {a,b,c}\".")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *PickerPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *PickerPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *PickerPlugin) RegisterWeb() *string {
return nil
}
func (p *PickerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

@ -1,15 +1,3 @@
// © 2013 the CatBase Authors under the WTFPL. See AUTHORS for the list of authors.
package plugins
import "github.com/velour/catbase/bot/msg"
// Plugin interface defines the methods needed to accept a plugin
type Plugin interface {
Message(message msg.Message) bool
Event(kind string, message msg.Message) bool
BotMessage(message msg.Message) bool
LoadData()
Help()
RegisterWeb()
}

View File

@ -11,36 +11,38 @@ import (
)
type ReactionPlugin struct {
Bot bot.Bot
Config *config.Config
bot bot.Bot
config *config.Config
}
func New(bot bot.Bot) *ReactionPlugin {
return &ReactionPlugin{
Bot: bot,
Config: bot.Config(),
func New(b bot.Bot) *ReactionPlugin {
rp := &ReactionPlugin{
bot: b,
config: b.Config(),
}
b.Register(rp, bot.Message, rp.message)
return rp
}
func (p *ReactionPlugin) Message(message msg.Message) bool {
func (p *ReactionPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
harrass := false
for _, nick := range p.Config.Reaction.HarrassList {
for _, nick := range p.config.GetArray("Reaction.HarrassList", []string{}) {
if message.User.Name == nick {
harrass = true
break
}
}
chance := p.Config.Reaction.GeneralChance
chance := p.config.GetFloat64("Reaction.GeneralChance", 0.01)
negativeWeight := 1
if harrass {
chance = p.Config.Reaction.HarrassChance
negativeWeight = p.Config.Reaction.NegativeHarrassmentMultiplier
chance = p.config.GetFloat64("Reaction.HarrassChance", 0.05)
negativeWeight = p.config.GetInt("Reaction.NegativeHarrassmentMultiplier", 2)
}
if rand.Float64() < chance {
numPositiveReactions := len(p.Config.Reaction.PositiveReactions)
numNegativeReactions := len(p.Config.Reaction.NegativeReactions)
numPositiveReactions := len(p.config.GetArray("Reaction.PositiveReactions", []string{}))
numNegativeReactions := len(p.config.GetArray("Reaction.NegativeReactions", []string{}))
maxIndex := numPositiveReactions + numNegativeReactions*negativeWeight
@ -49,33 +51,15 @@ func (p *ReactionPlugin) Message(message msg.Message) bool {
reaction := ""
if index < numPositiveReactions {
reaction = p.Config.Reaction.PositiveReactions[index]
reaction = p.config.GetArray("Reaction.PositiveReactions", []string{})[index]
} else {
index -= numPositiveReactions
index %= numNegativeReactions
reaction = p.Config.Reaction.NegativeReactions[index]
reaction = p.config.GetArray("Reaction.NegativeReactions", []string{})[index]
}
p.Bot.React(message.Channel, reaction, message)
p.bot.Send(c, bot.Reaction, message.Channel, reaction, message)
}
return false
}
func (p *ReactionPlugin) Help(channel string, parts []string) {
}
func (p *ReactionPlugin) Event(kind string, message msg.Message) bool {
return false
}
func (p *ReactionPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *ReactionPlugin) RegisterWeb() *string {
return nil
}
func (p *ReactionPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

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

View File

@ -5,13 +5,18 @@ package reminder
import (
"errors"
"fmt"
"log"
"strconv"
"strings"
"sync"
"time"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog/log"
"github.com/olebedev/when"
"github.com/olebedev/when/rules/common"
"github.com/olebedev/when/rules/en"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
@ -22,11 +27,12 @@ const (
)
type ReminderPlugin struct {
Bot bot.Bot
bot bot.Bot
db *sqlx.DB
mutex *sync.Mutex
timer *time.Timer
config *config.Config
when *when.Parser
}
type Reminder struct {
@ -38,10 +44,8 @@ type Reminder struct {
channel string
}
func New(bot bot.Bot) *ReminderPlugin {
log.SetFlags(log.LstdFlags | log.Lshortfile)
if bot.DBVersion() == 1 {
if _, err := bot.DB().Exec(`create table if not exists reminders (
func New(b bot.Bot) *ReminderPlugin {
if _, err := b.DB().Exec(`create table if not exists reminders (
id integer primary key,
fromWho string,
toWho string,
@ -49,33 +53,51 @@ func New(bot bot.Bot) *ReminderPlugin {
remindWhen string,
channel string
);`); err != nil {
log.Fatal(err)
}
log.Fatal().Err(err)
}
dur, _ := time.ParseDuration("1h")
timer := time.NewTimer(dur)
timer.Stop()
w := when.New(nil)
w.Add(en.All...)
w.Add(common.All...)
plugin := &ReminderPlugin{
Bot: bot,
db: bot.DB(),
bot: b,
db: b.DB(),
mutex: &sync.Mutex{},
timer: timer,
config: bot.Config(),
config: b.Config(),
when: w,
}
plugin.queueUpNextReminder()
go reminderer(plugin)
go reminderer(b.DefaultConnector(), plugin)
b.Register(plugin, bot.Message, plugin.message)
b.Register(plugin, bot.Help, plugin.help)
return plugin
}
func (p *ReminderPlugin) Message(message msg.Message) bool {
func (p *ReminderPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
channel := message.Channel
from := message.User.Name
var dur, dur2 time.Duration
t, err := p.when.Parse(message.Body, time.Now())
// Allowing err to fallthrough for other parsing
if t != nil && err == nil {
t2 := t.Time.Sub(time.Now()).String()
message.Body = string(message.Body[0:t.Index]) + t2 + string(message.Body[t.Index+len(t.Text):])
log.Debug().
Str("body", message.Body).
Str("text", t.Text).
Msg("Got time request")
}
parts := strings.Fields(message.Body)
if len(parts) >= 5 {
@ -85,17 +107,16 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
who = from
}
dur, err := time.ParseDuration(parts[3])
dur, err = time.ParseDuration(parts[3])
if err != nil {
p.Bot.SendMessage(channel, "Easy cowboy, not sure I can parse that duration.")
p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
return true
}
operator := strings.ToLower(parts[2])
doConfirm := true
if operator == "in" {
if operator == "in" || operator == "at" || operator == "on" {
//one off reminder
//remind who in dur blah
when := time.Now().UTC().Add(dur)
@ -113,9 +134,10 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
} else if operator == "every" && strings.ToLower(parts[4]) == "for" {
//batch add, especially for reminding msherms to buy a kit
//remind who every dur for dur2 blah
dur2, err := time.ParseDuration(parts[5])
dur2, err = time.ParseDuration(parts[5])
if err != nil {
p.Bot.SendMessage(channel, "Easy cowboy, not sure I can parse that duration.")
log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
return true
}
@ -123,9 +145,10 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
endTime := time.Now().UTC().Add(dur2)
what := strings.Join(parts[6:], " ")
max := p.config.GetInt("Reminder.MaxBatchAdd", 10)
for i := 0; when.Before(endTime); i++ {
if i >= p.config.Reminder.MaxBatchAdd {
p.Bot.SendMessage(channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.")
if i >= max {
p.bot.Send(c, bot.Message, channel, "Easy cowboy, that's a lot of reminders. I'll add some of them.")
doConfirm = false
break
}
@ -142,14 +165,14 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
when = when.Add(dur)
}
} else {
p.Bot.SendMessage(channel, "Easy cowboy, not sure I comprehend what you're asking.")
p.bot.Send(c, bot.Message, channel, "Easy cowboy, not sure I comprehend what you're asking.")
return true
}
if doConfirm && from == who {
p.Bot.SendMessage(channel, fmt.Sprintf("Okay. I'll remind you."))
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Okay. I'll remind you."))
} else if doConfirm {
p.Bot.SendMessage(channel, fmt.Sprintf("Sure %s, I'll remind %s.", from, who))
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Sure %s, I'll remind %s.", from, who))
}
p.queueUpNextReminder()
@ -169,22 +192,22 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
}
}
if err != nil {
p.Bot.SendMessage(channel, "listing failed.")
p.bot.Send(c, bot.Message, channel, "listing failed.")
} else {
p.Bot.SendMessage(channel, response)
p.bot.Send(c, bot.Message, channel, response)
}
return true
} else if len(parts) == 3 && strings.ToLower(parts[0]) == "cancel" && strings.ToLower(parts[1]) == "reminder" {
id, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
p.Bot.SendMessage(channel, fmt.Sprintf("couldn't parse id: %s", parts[2]))
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("couldn't parse id: %s", parts[2]))
} else {
err := p.deleteReminder(id)
if err == nil {
p.Bot.SendMessage(channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2]))
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("successfully canceled reminder: %s", parts[2]))
} else {
p.Bot.SendMessage(channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2]))
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("failed to find and cancel reminder: %s", parts[2]))
}
}
return true
@ -193,20 +216,9 @@ func (p *ReminderPlugin) Message(message msg.Message) bool {
return false
}
func (p *ReminderPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Pester someone with a reminder. Try \"remind <user> in <duration> message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration")
}
func (p *ReminderPlugin) Event(kind string, message msg.Message) bool {
return false
}
func (p *ReminderPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *ReminderPlugin) RegisterWeb() *string {
return nil
func (p *ReminderPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "Pester someone with a reminder. Try \"remind <user> in <duration> message\".\n\nUnsure about duration syntax? Check https://golang.org/pkg/time/#ParseDuration")
return true
}
func (p *ReminderPlugin) getNextReminder() *Reminder {
@ -214,7 +226,7 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
defer p.mutex.Unlock()
rows, err := p.db.Query("select id, fromWho, toWho, what, remindWhen, channel from reminders order by remindWhen asc limit 1;")
if err != nil {
log.Print(err)
log.Error().Err(err)
return nil
}
defer rows.Close()
@ -223,19 +235,19 @@ func (p *ReminderPlugin) getNextReminder() *Reminder {
var reminder *Reminder
for rows.Next() {
if once {
log.Print("somehow got multiple rows")
log.Debug().Msg("somehow got multiple rows")
}
reminder = &Reminder{}
var when string
err := rows.Scan(&reminder.id, &reminder.from, &reminder.who, &reminder.what, &when, &reminder.channel)
if err != nil {
log.Print(err)
log.Error().Err(err)
return nil
}
reminder.when, err = time.Parse(TIMESTAMP, when)
if err != nil {
log.Print(err)
log.Error().Err(err)
return nil
}
@ -252,7 +264,7 @@ func (p *ReminderPlugin) addReminder(reminder *Reminder) error {
reminder.from, reminder.who, reminder.what, reminder.when.Format(TIMESTAMP), reminder.channel)
if err != nil {
log.Print(err)
log.Error().Err(err)
}
return err
}
@ -262,7 +274,7 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
defer p.mutex.Unlock()
res, err := p.db.Exec(`delete from reminders where id = ?;`, id)
if err != nil {
log.Print(err)
log.Error().Err(err)
} else {
if affected, err := res.RowsAffected(); err != nil {
return err
@ -273,12 +285,28 @@ func (p *ReminderPlugin) deleteReminder(id int64) error {
return err
}
func (p *ReminderPlugin) getRemindersFormatted(queryString string) (string, error) {
func (p *ReminderPlugin) getRemindersFormatted(filter string) (string, error) {
max := p.config.GetInt("Reminder.MaxList", 25)
queryString := fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders %s order by remindWhen asc limit %d;", filter, max)
countString := fmt.Sprintf("select COUNT(*) from reminders %s;", filter)
p.mutex.Lock()
defer p.mutex.Unlock()
var total int
err := p.db.Get(&total, countString)
if err != nil {
log.Error().Err(err)
return "", nil
}
if total == 0 {
return "no pending reminders", nil
}
rows, err := p.db.Query(queryString)
if err != nil {
log.Print(err)
log.Error().Err(err)
return "", nil
}
defer rows.Close()
@ -294,23 +322,25 @@ func (p *ReminderPlugin) getRemindersFormatted(queryString string) (string, erro
reminders += fmt.Sprintf("%d) %s -> %s :: %s @ %s (%d)\n", counter, reminder.from, reminder.who, reminder.what, when, reminder.id)
counter++
}
if counter == 1 {
return "no pending reminders", nil
remaining := total - max
if remaining > 0 {
reminders += fmt.Sprintf("...%d more...\n", remaining)
}
return reminders, nil
}
func (p *ReminderPlugin) getAllRemindersFormatted(channel string) (string, error) {
return p.getRemindersFormatted("select id, fromWho, toWho, what, remindWhen from reminders order by remindWhen asc;")
return p.getRemindersFormatted("")
}
func (p *ReminderPlugin) getAllRemindersFromMeFormatted(channel, me string) (string, error) {
return p.getRemindersFormatted(fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders where fromWho = '%s' order by remindWhen asc;", me))
return p.getRemindersFormatted(fmt.Sprintf("where fromWho = '%s'", me))
}
func (p *ReminderPlugin) getAllRemindersToMeFormatted(channel, me string) (string, error) {
return p.getRemindersFormatted(fmt.Sprintf("select id, fromWho, toWho, what, remindWhen from reminders where toWho = '%s' order by remindWhen asc;", me))
return p.getRemindersFormatted(fmt.Sprintf("where toWho = '%s'", me))
}
func (p *ReminderPlugin) queueUpNextReminder() {
@ -321,7 +351,7 @@ func (p *ReminderPlugin) queueUpNextReminder() {
}
}
func reminderer(p *ReminderPlugin) {
func reminderer(c bot.Connector, p *ReminderPlugin) {
for {
<-p.timer.C
@ -336,17 +366,16 @@ func reminderer(p *ReminderPlugin) {
message = fmt.Sprintf("Hey %s, %s wanted you to be reminded: %s", reminder.who, reminder.from, reminder.what)
}
p.Bot.SendMessage(reminder.channel, message)
p.bot.Send(c, bot.Message, reminder.channel, message)
if err := p.deleteReminder(reminder.id); err != nil {
log.Print(reminder.id)
log.Print(err)
log.Fatal("this will cause problems, we need to stop now.")
log.Error().
Int64("id", reminder.id).
Err(err).
Msg("this will cause problems, we need to stop now.")
}
}
p.queueUpNextReminder()
}
}
func (p *ReminderPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -4,6 +4,7 @@ package reminder
import (
"fmt"
"github.com/velour/catbase/plugins/cli"
"strings"
"testing"
"time"
@ -14,25 +15,16 @@ import (
"github.com/velour/catbase/bot/user"
)
func makeMessage(payload string) msg.Message {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
Command: isCmd,
}
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
return makeMessageBy(payload, "tester")
}
func makeMessageBy(payload, by string) msg.Message {
func makeMessageBy(payload, by string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: by},
Channel: "test",
Body: payload,
@ -40,11 +32,16 @@ func makeMessageBy(payload, by string) msg.Message {
}
}
func TestMeReminder(t *testing.T) {
func setup(t *testing.T) (*ReminderPlugin, *bot.MockBot) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind me in 1s don't fail this test"))
r := New(mb)
mb.DB().MustExec(`delete from reminders; delete from config;`)
return r, mb
}
func TestMeReminder(t *testing.T) {
c, mb := setup(t)
res := c.message(makeMessage("!remind me in 1s don't fail this test"))
time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
@ -53,10 +50,8 @@ func TestMeReminder(t *testing.T) {
}
func TestReminder(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 1s don't fail this test"))
c, mb := setup(t)
res := c.message(makeMessage("!remind testuser in 1s don't fail this test"))
time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 2)
assert.True(t, res)
@ -65,12 +60,10 @@ func TestReminder(t *testing.T) {
}
func TestReminderReorder(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 2s don't fail this test 2"))
c, mb := setup(t)
res := c.message(makeMessage("!remind testuser in 2s don't fail this test 2"))
assert.True(t, res)
res = c.Message(makeMessage("!remind testuser in 1s don't fail this test 1"))
res = c.message(makeMessage("!remind testuser in 1s don't fail this test 1"))
assert.True(t, res)
time.Sleep(5 * time.Second)
assert.Len(t, mb.Messages, 4)
@ -81,34 +74,28 @@ func TestReminderReorder(t *testing.T) {
}
func TestReminderParse(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in unparseable don't fail this test"))
c, mb := setup(t)
res := c.message(makeMessage("!remind testuser in unparseable don't fail this test"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "Easy cowboy, not sure I can parse that duration.")
assert.Contains(t, mb.Messages[0], "Easy cowboy, not sure I can parse that duration. Try something like '1.5h' or '2h45m'.")
}
func TestEmptyList(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!list reminders"))
c, mb := setup(t)
res := c.message(makeMessage("!list reminders"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "no pending reminders")
}
func TestList(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 5m don't fail this test 1"))
c, mb := setup(t)
res := c.message(makeMessage("!remind testuser in 5m don't fail this test 1"))
assert.True(t, res)
res = c.Message(makeMessage("!remind testuser in 5m don't fail this test 2"))
res = c.message(makeMessage("!remind testuser in 5m don't fail this test 2"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders"))
res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "1) tester -> testuser :: don't fail this test 1 @ ")
@ -116,14 +103,12 @@ func TestList(t *testing.T) {
}
func TestListBy(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
c, mb := setup(t)
res := c.message(makeMessageBy("!remind testuser in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders from testuser"))
res = c.message(makeMessage("!list reminders from testuser"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "don't fail this test 1 @ ")
@ -131,14 +116,12 @@ func TestListBy(t *testing.T) {
}
func TestListTo(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
c, mb := setup(t)
res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders to testuser"))
res = c.message(makeMessage("!list reminders to testuser"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.NotContains(t, mb.Messages[2], "don't fail this test 1 @ ")
@ -146,55 +129,36 @@ func TestListTo(t *testing.T) {
}
func TestToEmptyList(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
c, mb := setup(t)
res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders to test"))
res = c.message(makeMessage("!list reminders to test"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders")
}
func TestFromEmptyList(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
c, mb := setup(t)
res := c.message(makeMessageBy("!remind testuser2 in 5m don't fail this test 1", "testuser"))
assert.True(t, res)
res = c.Message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
res = c.message(makeMessageBy("!remind testuser in 5m don't fail this test 2", "testuser2"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders from test"))
res = c.message(makeMessage("!list reminders from test"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[2], "no pending reminders")
}
func TestBatch(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c.config.Reminder.MaxBatchAdd = 50
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser every 1ms for 5ms yikes"))
assert.True(t, res)
time.Sleep(2 * time.Second)
assert.Len(t, mb.Messages, 6)
for i := 0; i < 5; i++ {
assert.Contains(t, mb.Messages[i+1], "Hey testuser, tester wanted you to be reminded: yikes")
}
}
func TestBatchMax(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c.config.Reminder.MaxBatchAdd = 10
c, mb := setup(t)
c.config.Set("Reminder.MaxBatchAdd", "10")
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser every 1h for 24h yikes"))
res := c.message(makeMessage("!remind testuser every 1h for 24h yikes"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders"))
res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
time.Sleep(6 * time.Second)
assert.Len(t, mb.Messages, 2)
@ -206,14 +170,13 @@ func TestBatchMax(t *testing.T) {
}
func TestCancel(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c, mb := setup(t)
assert.NotNil(t, c)
res := c.Message(makeMessage("!remind testuser in 1m don't fail this test"))
res := c.message(makeMessage("!remind testuser in 1m don't fail this test"))
assert.True(t, res)
res = c.Message(makeMessage("!cancel reminder 1"))
res = c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res)
res = c.Message(makeMessage("!list reminders"))
res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
assert.Len(t, mb.Messages, 3)
assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.")
@ -222,40 +185,45 @@ func TestCancel(t *testing.T) {
}
func TestCancelMiss(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c, mb := setup(t)
assert.NotNil(t, c)
res := c.Message(makeMessage("!cancel reminder 1"))
res := c.message(makeMessage("!cancel reminder 1"))
assert.True(t, res)
assert.Len(t, mb.Messages, 1)
assert.Contains(t, mb.Messages[0], "failed to find and cancel reminder: 1")
}
func TestHelp(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
func TestLimitList(t *testing.T) {
c, mb := setup(t)
c.config.Set("Reminder.MaxBatchAdd", "10")
c.config.Set("Reminder.MaxList", "25")
assert.NotNil(t, c)
c.Help("channel", []string{})
//Someone can redo this with a single batch add, but I can't locally due to an old version of sqllite (maybe).
res := c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
assert.True(t, res)
res = c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
assert.True(t, res)
res = c.message(makeMessage("!remind testuser every 1h for 10h don't fail this test"))
assert.True(t, res)
res = c.message(makeMessage("!list reminders"))
assert.True(t, res)
assert.Len(t, mb.Messages, 4)
assert.Contains(t, mb.Messages[0], "Sure tester, I'll remind testuser.")
assert.Contains(t, mb.Messages[1], "Sure tester, I'll remind testuser.")
assert.Contains(t, mb.Messages[2], "Sure tester, I'll remind testuser.")
for i := 0; i < 25; i++ {
assert.Contains(t, mb.Messages[3], fmt.Sprintf("%d) tester -> testuser :: don't fail this test", i+1))
}
assert.Contains(t, mb.Messages[3], "more...")
assert.NotContains(t, mb.Messages[3], "26) tester -> testuser")
}
func TestHelp(t *testing.T) {
c, mb := setup(t)
assert.NotNil(t, c)
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
func TestBotMessage(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.False(t, c.BotMessage(makeMessage("test")))
}
func TestEvent(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.False(t, c.Event("dummy", makeMessage("test")))
}
func TestRegisterWeb(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.Nil(t, c.RegisterWeb())
}

View File

@ -20,7 +20,7 @@ const (
)
type RPGPlugin struct {
Bot bot.Bot
bot bot.Bot
listenFor map[string]*board
}
@ -98,45 +98,35 @@ func (b *board) checkAndMove(dx, dy int) int {
}
func New(b bot.Bot) *RPGPlugin {
return &RPGPlugin{
Bot: b,
rpg := &RPGPlugin{
bot: b,
listenFor: map[string]*board{},
}
b.Register(rpg, bot.Message, rpg.message)
b.Register(rpg, bot.Reply, rpg.replyMessage)
b.Register(rpg, bot.Help, rpg.help)
return rpg
}
func (p *RPGPlugin) Message(message msg.Message) bool {
func (p *RPGPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.ToLower(message.Body) == "start rpg" {
b := NewRandomBoard()
ts := p.Bot.SendMessage(message.Channel, b.toMessageString())
ts, _ := p.bot.Send(c, bot.Message, message.Channel, b.toMessageString())
p.listenFor[ts] = b
p.Bot.ReplyToMessageIdentifier(message.Channel, "Over here.", ts)
p.bot.Send(c, bot.Reply, message.Channel, "Over here.", ts)
return true
}
return false
}
func (p *RPGPlugin) LoadData() {
func (p *RPGPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "Go find a walkthrough or something.")
return true
}
func (p *RPGPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Go find a walkthrough or something.")
}
func (p *RPGPlugin) Event(kind string, message msg.Message) bool {
return false
}
func (p *RPGPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *RPGPlugin) RegisterWeb() *string {
return nil
}
func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool {
if strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) {
func (p *RPGPlugin) replyMessage(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
identifier := args[0].(string)
if strings.ToLower(message.User.Name) != strings.ToLower(p.bot.Config().Get("Nick", "bot")) {
if b, ok := p.listenFor[identifier]; ok {
var res int
@ -155,12 +145,12 @@ func (p *RPGPlugin) ReplyMessage(message msg.Message, identifier string) bool {
switch res {
case OK:
p.Bot.Edit(message.Channel, b.toMessageString(), identifier)
p.bot.Send(c, bot.Edit, message.Channel, b.toMessageString(), identifier)
case WIN:
p.Bot.Edit(message.Channel, b.toMessageString(), identifier)
p.Bot.ReplyToMessageIdentifier(message.Channel, "congratulations, you beat the easiest level imaginable.", identifier)
p.bot.Send(c, bot.Edit, message.Channel, b.toMessageString(), identifier)
p.bot.Send(c, bot.Reply, message.Channel, "congratulations, you beat the easiest level imaginable.", identifier)
case INVALID:
p.Bot.ReplyToMessageIdentifier(message.Channel, fmt.Sprintf("you can't move %s", message.Body), identifier)
p.bot.Send(c, bot.Reply, message.Channel, fmt.Sprintf("you can't move %s", message.Body), identifier)
}
return true
}

View File

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

View File

@ -12,7 +12,7 @@ import (
)
type RSSPlugin struct {
Bot bot.Bot
bot bot.Bot
cache map[string]*cacheItem
shelfLife time.Duration
maxLines int
@ -49,28 +49,31 @@ func (c *cacheItem) getCurrentPage(maxLines int) string {
return page
}
func New(bot bot.Bot) *RSSPlugin {
return &RSSPlugin{
Bot: bot,
func New(b bot.Bot) *RSSPlugin {
rss := &RSSPlugin{
bot: b,
cache: map[string]*cacheItem{},
shelfLife: time.Minute * 20,
maxLines: 5,
shelfLife: time.Minute * time.Duration(b.Config().GetInt("rss.shelfLife", 20)),
maxLines: b.Config().GetInt("rss.maxLines", 5),
}
b.Register(rss, bot.Message, rss.message)
b.Register(rss, bot.Help, rss.help)
return rss
}
func (p *RSSPlugin) Message(message msg.Message) bool {
func (p *RSSPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
tokens := strings.Fields(message.Body)
numTokens := len(tokens)
if numTokens == 2 && strings.ToLower(tokens[0]) == "rss" {
if item, ok := p.cache[strings.ToLower(tokens[1])]; ok && time.Now().Before(item.expiration) {
p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines))
p.bot.Send(c, bot.Message, message.Channel, item.getCurrentPage(p.maxLines))
return true
} else {
fp := gofeed.NewParser()
feed, err := fp.ParseURL(tokens[1])
if err != nil {
p.Bot.SendMessage(message.Channel, fmt.Sprintf("RSS error: %s", err.Error()))
p.bot.Send(c, bot.Message, message.Channel, fmt.Sprintf("RSS error: %s", err.Error()))
return true
}
item := &cacheItem{
@ -86,7 +89,7 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
p.cache[strings.ToLower(tokens[1])] = item
p.Bot.SendMessage(message.Channel, item.getCurrentPage(p.maxLines))
p.bot.Send(c, bot.Message, message.Channel, item.getCurrentPage(p.maxLines))
return true
}
}
@ -94,28 +97,8 @@ func (p *RSSPlugin) Message(message msg.Message) bool {
return false
}
func (p *RSSPlugin) LoadData() {
// This bot has no data to load
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *RSSPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "try '!rss http://rss.cnn.com/rss/edition.rss'")
func (p *RSSPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "try '!rss http://rss.cnn.com/rss/edition.rss'")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *RSSPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *RSSPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *RSSPlugin) RegisterWeb() *string {
return nil
}
func (p *RSSPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

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

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 (
"fmt"
"math/rand"
"io/ioutil"
"net/http"
"os"
"os/exec"
"strings"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
)
var goatse []string = []string{
var goatse = []string{
"```* g o a t s e x * g o a t s e x * g o a t s e x *",
"g g",
"o / \\ \\ / \\ o",
@ -40,28 +46,48 @@ var goatse []string = []string{
}
type TalkerPlugin struct {
Bot bot.Bot
enforceNicks bool
sayings []string
bot bot.Bot
config *config.Config
sayings []string
}
func New(bot bot.Bot) *TalkerPlugin {
return &TalkerPlugin{
Bot: bot,
enforceNicks: bot.Config().EnforceNicks,
sayings: bot.Config().WelcomeMsgs,
func New(b bot.Bot) *TalkerPlugin {
tp := &TalkerPlugin{
bot: b,
config: b.Config(),
}
b.Register(tp, bot.Message, tp.message)
b.Register(tp, bot.Help, tp.help)
tp.registerWeb(b.DefaultConnector())
return tp
}
func (p *TalkerPlugin) Message(message msg.Message) bool {
func (p *TalkerPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
channel := message.Channel
body := message.Body
lowermessage := strings.ToLower(body)
if message.Command && strings.HasPrefix(lowermessage, "cowsay") {
msg, err := p.cowSay(strings.TrimPrefix(message.Body, "cowsay "))
if err != nil {
p.bot.Send(c, bot.Message, channel, "Error running cowsay: %s", err)
return true
}
p.bot.Send(c, bot.Message, channel, msg)
return true
}
if message.Command && strings.HasPrefix(lowermessage, "list cows") {
cows := p.allCows()
m := fmt.Sprintf("Cows: %s", strings.Join(cows, ", "))
p.bot.Send(c, bot.Message, channel, m)
return true
}
// TODO: This ought to be space split afterwards to remove any punctuation
if message.Command && strings.HasPrefix(lowermessage, "say") {
msg := strings.TrimSpace(body[3:])
p.Bot.SendMessage(channel, msg)
p.bot.Send(c, bot.Message, channel, msg)
return true
}
@ -77,45 +103,85 @@ func (p *TalkerPlugin) Message(message msg.Message) bool {
line = strings.Replace(line, "{nick}", nick, 1)
output += line + "\n"
}
p.Bot.SendMessage(channel, output)
return true
}
if p.enforceNicks && len(message.User.Name) != 9 {
msg := fmt.Sprintf("Hey %s, we really like to have 9 character nicks because we're crazy OCD and stuff.",
message.User.Name)
p.Bot.SendMessage(message.Channel, msg)
p.bot.Send(c, bot.Message, channel, output)
return true
}
return false
}
func (p *TalkerPlugin) Help(channel string, parts []string) {
p.Bot.SendMessage(channel, "Hi, this is talker. I like to talk about FredFelps!")
func (p *TalkerPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "Hi, this is talker. I like to talk about FredFelps!")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *TalkerPlugin) Event(kind string, message msg.Message) bool {
if kind == "JOIN" && strings.ToLower(message.User.Name) != strings.ToLower(p.Bot.Config().Nick) {
if len(p.sayings) == 0 {
return false
func (p *TalkerPlugin) cowSay(text string) (string, error) {
fields := strings.Split(text, " ")
cow := "default"
if len(fields) > 1 && p.hasCow(fields[0]) {
cow = fields[0]
text = strings.Join(fields[1:], " ")
}
cmd := exec.Command("cowsay", "-f", cow, text)
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
if err = cmd.Start(); err != nil {
return "", err
}
output, err := ioutil.ReadAll(stdout)
if err != nil {
return "", err
}
return fmt.Sprintf("```%s```", output), nil
}
func (p *TalkerPlugin) hasCow(cow string) bool {
cows := p.allCows()
for _, c := range cows {
if strings.ToLower(cow) == c {
return true
}
msg := fmt.Sprintf(p.sayings[rand.Intn(len(p.sayings))], message.User.Name)
p.Bot.SendMessage(message.Channel, msg)
return true
}
return false
}
// Handler for bot's own messages
func (p *TalkerPlugin) BotMessage(message msg.Message) bool {
return false
func (p *TalkerPlugin) allCows() []string {
f, err := os.Open(p.config.Get("talker.cowpath", "/usr/local/share/cows"))
if err != nil {
return []string{"default"}
}
files, err := f.Readdir(0)
if err != nil {
return []string{"default"}
}
cows := []string{}
for _, f := range files {
if strings.HasSuffix(f.Name(), ".cow") {
cows = append(cows, strings.TrimSuffix(f.Name(), ".cow"))
}
}
return cows
}
// Register any web URLs desired
func (p *TalkerPlugin) RegisterWeb() *string {
return nil
func (p *TalkerPlugin) registerWeb(c bot.Connector) {
http.HandleFunc("/slash/cowsay", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
log.Debug().Msgf("Cowsay:\n%+v", r.PostForm.Get("text"))
channel := r.PostForm.Get("channel_id")
log.Debug().Msgf("channel: %s", channel)
msg, err := p.cowSay(r.PostForm.Get("text"))
if err != nil {
p.bot.Send(c, bot.Message, channel, fmt.Sprintf("Error running cowsay: %s", err))
return
}
p.bot.Send(c, bot.Message, channel, msg)
w.WriteHeader(200)
})
}
func (p *TalkerPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

@ -3,6 +3,7 @@
package talker
import (
"github.com/velour/catbase/plugins/cli"
"strings"
"testing"
@ -12,12 +13,12 @@ import (
"github.com/velour/catbase/bot/user"
)
func makeMessage(payload string) msg.Message {
func makeMessage(payload string) (bot.Connector, bot.Kind, msg.Message) {
isCmd := strings.HasPrefix(payload, "!")
if isCmd {
payload = payload[1:]
}
return msg.Message{
return &cli.CliPlugin{}, bot.Message, msg.Message{
User: &user.User{Name: "tester"},
Channel: "test",
Body: payload,
@ -29,7 +30,7 @@ func TestGoatse(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("goatse"))
res := c.message(makeMessage("goatse"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
@ -38,7 +39,7 @@ func TestGoatseCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!goatse"))
res := c.message(makeMessage("!goatse"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e")
@ -48,7 +49,7 @@ func TestGoatseWithNickCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!goatse seabass"))
res := c.message(makeMessage("!goatse seabass"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "g o a t s e")
@ -59,7 +60,7 @@ func TestSay(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("say hello"))
res := c.message(makeMessage("say hello"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
@ -68,78 +69,16 @@ func TestSayCommand(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Message(makeMessage("!say hello"))
res := c.message(makeMessage("!say hello"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "hello")
}
func TestNineChars(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c.enforceNicks = true
assert.NotNil(t, c)
res := c.Message(makeMessage("hello there"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "OCD")
}
func TestWelcome(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c.sayings = []string{"Hi"}
assert.NotNil(t, c)
res := c.Event("JOIN", makeMessage("hello there"))
assert.Len(t, mb.Messages, 1)
assert.True(t, res)
assert.Contains(t, mb.Messages[0], "Hi")
}
func TestNoSayings(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
c.sayings = []string{}
assert.NotNil(t, c)
res := c.Event("JOIN", makeMessage("hello there"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
func TestNonJoinEvent(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
res := c.Event("SPLURT", makeMessage("hello there"))
assert.Len(t, mb.Messages, 0)
assert.False(t, res)
}
func TestHelp(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
c.Help("channel", []string{})
c.help(&cli.CliPlugin{}, bot.Help, msg.Message{Channel: "channel"}, []string{})
assert.Len(t, mb.Messages, 1)
}
func TestBotMessage(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.False(t, c.BotMessage(makeMessage("test")))
}
func TestEvent(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.False(t, c.Event("dummy", makeMessage("test")))
}
func TestRegisterWeb(t *testing.T) {
mb := bot.NewMockBot()
c := New(mb)
assert.NotNil(t, c)
assert.Nil(t, c.RegisterWeb())
}

View File

@ -16,32 +16,28 @@ type TellPlugin struct {
}
func New(b bot.Bot) *TellPlugin {
return &TellPlugin{b, make(map[string][]string)}
tp := &TellPlugin{b, make(map[string][]string)}
b.Register(tp, bot.Message, tp.message)
return tp
}
func (t *TellPlugin) Message(message msg.Message) bool {
func (t *TellPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
if strings.HasPrefix(strings.ToLower(message.Body), "tell") {
parts := strings.Split(message.Body, " ")
target := strings.ToLower(parts[1])
newMessage := strings.Join(parts[2:], " ")
newMessage = fmt.Sprintf("Hey, %s. %s said: %s", target, message.User.Name, newMessage)
t.users[target] = append(t.users[target], newMessage)
t.b.SendMessage(message.Channel, fmt.Sprintf("Okay. I'll tell %s.", target))
t.b.Send(c, bot.Message, message.Channel, fmt.Sprintf("Okay. I'll tell %s.", target))
return true
}
uname := strings.ToLower(message.User.Name)
if msg, ok := t.users[uname]; ok && len(msg) > 0 {
for _, m := range msg {
t.b.SendMessage(message.Channel, string(m))
t.b.Send(c, bot.Message, message.Channel, string(m))
}
t.users[uname] = []string{}
return true
}
return false
}
func (t *TellPlugin) Event(kind string, message msg.Message) bool { return false }
func (t *TellPlugin) ReplyMessage(msg.Message, string) bool { return false }
func (t *TellPlugin) BotMessage(message msg.Message) bool { return false }
func (t *TellPlugin) Help(channel string, parts []string) {}
func (t *TellPlugin) RegisterWeb() *string { return nil }

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
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"log"
"net/http"
"net/url"
"strings"
"text/template"
"time"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
)
const (
isStreamingTplFallback = "{{.Name}} is streaming {{.Game}} at {{.URL}}"
notStreamingTplFallback = "{{.Name}} is not streaming"
stoppedStreamingTplFallback = "{{.Name}} just stopped streaming"
)
type TwitchPlugin struct {
Bot bot.Bot
bot bot.Bot
config *config.Config
twitchList map[string]*Twitcher
}
type Twitcher struct {
name string
game string
name string
gameID string
}
func (t Twitcher) URL() string {
@ -51,39 +58,34 @@ type stream struct {
} `json:"pagination"`
}
func New(bot bot.Bot) *TwitchPlugin {
func New(b bot.Bot) *TwitchPlugin {
p := &TwitchPlugin{
Bot: bot,
config: bot.Config(),
bot: b,
config: b.Config(),
twitchList: map[string]*Twitcher{},
}
for _, users := range p.config.Twitch.Users {
for _, twitcherName := range users {
for _, ch := range p.config.GetArray("Twitch.Channels", []string{}) {
for _, twitcherName := range p.config.GetArray("Twitch."+ch+".Users", []string{}) {
if _, ok := p.twitchList[twitcherName]; !ok {
p.twitchList[twitcherName] = &Twitcher{
name: twitcherName,
game: "",
name: twitcherName,
gameID: "",
}
}
}
go p.twitchLoop(b.DefaultConnector(), ch)
}
for channel := range p.config.Twitch.Users {
go p.twitchLoop(channel)
}
b.Register(p, bot.Message, p.message)
b.Register(p, bot.Help, p.help)
p.registerWeb()
return p
}
func (p *TwitchPlugin) BotMessage(message msg.Message) bool {
return false
}
func (p *TwitchPlugin) RegisterWeb() *string {
func (p *TwitchPlugin) registerWeb() {
http.HandleFunc("/isstreaming/", p.serveStreaming)
tmp := "/isstreaming"
return &tmp
}
func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
@ -101,61 +103,68 @@ func (p *TwitchPlugin) serveStreaming(w http.ResponseWriter, r *http.Request) {
}
status := "NO."
if twitcher.game != "" {
if twitcher.gameID != "" {
status = "YES."
}
context := map[string]interface{}{"Name": twitcher.name, "Status": status}
t, err := template.New("streaming").Parse(page)
if err != nil {
log.Println("Could not parse template!", err)
log.Error().Err(err).Msg("Could not parse template!")
return
}
err = t.Execute(w, context)
if err != nil {
log.Println("Could not execute template!", err)
log.Error().Err(err).Msg("Could not execute template!")
}
}
func (p *TwitchPlugin) Message(message msg.Message) bool {
if strings.ToLower(message.Body) == "twitch status" {
func (p *TwitchPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
body := strings.ToLower(message.Body)
if body == "twitch status" {
channel := message.Channel
if _, ok := p.config.Twitch.Users[channel]; ok {
for _, twitcherName := range p.config.Twitch.Users[channel] {
if _, ok = p.twitchList[twitcherName]; ok {
p.checkTwitch(channel, p.twitchList[twitcherName], true)
if users := p.config.GetArray("Twitch."+channel+".Users", []string{}); len(users) > 0 {
for _, twitcherName := range users {
if _, ok := p.twitchList[twitcherName]; ok {
p.checkTwitch(c, channel, p.twitchList[twitcherName], true)
}
}
}
return true
} else if body == "reset twitch" {
p.config.Set("twitch.istpl", isStreamingTplFallback)
p.config.Set("twitch.nottpl", notStreamingTplFallback)
p.config.Set("twitch.stoppedtpl", stoppedStreamingTplFallback)
}
return false
}
func (p *TwitchPlugin) Event(kind string, message msg.Message) bool {
return false
func (p *TwitchPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
msg := "You can set the templates for streams with\n"
msg += fmt.Sprintf("twitch.istpl (default: %s)\n", isStreamingTplFallback)
msg += fmt.Sprintf("twitch.nottpl (default: %s)\n", notStreamingTplFallback)
msg += fmt.Sprintf("twitch.stoppedtpl (default: %s)\n", stoppedStreamingTplFallback)
msg += "You can reset all messages with `!reset twitch`"
msg += "And you can ask who is streaming with `!twitch status`"
p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
func (p *TwitchPlugin) LoadData() {
func (p *TwitchPlugin) twitchLoop(c bot.Connector, channel string) {
frequency := p.config.GetInt("Twitch.Freq", 60)
if p.config.Get("twitch.clientid", "") == "" || p.config.Get("twitch.authorization", "") == "" {
log.Info().Msgf("Disabling twitch autochecking.")
return
}
}
func (p *TwitchPlugin) Help(channel string, parts []string) {
msg := "There's no help for you here."
p.Bot.SendMessage(channel, msg)
}
func (p *TwitchPlugin) twitchLoop(channel string) {
frequency := p.config.Twitch.Freq
log.Println("Checking every ", frequency, " seconds")
log.Info().Msgf("Checking every %d seconds", frequency)
for {
time.Sleep(time.Duration(frequency) * time.Second)
for _, twitcherName := range p.config.Twitch.Users[channel] {
p.checkTwitch(channel, p.twitchList[twitcherName], false)
for _, twitcherName := range p.config.GetArray("Twitch."+channel+".Users", []string{}) {
p.checkTwitch(c, channel, p.twitchList[twitcherName], false)
}
}
}
@ -184,14 +193,14 @@ func getRequest(url, clientID, authorization string) ([]byte, bool) {
return body, true
errCase:
log.Println(err)
log.Error().Err(err)
return []byte{}, false
}
func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPrintStatus bool) {
func (p *TwitchPlugin) checkTwitch(c bot.Connector, channel string, twitcher *Twitcher, alwaysPrintStatus bool) {
baseURL, err := url.Parse("https://api.twitch.tv/helix/streams")
if err != nil {
log.Println("Error parsing twitch stream URL")
log.Error().Msg("Error parsing twitch stream URL")
return
}
@ -200,8 +209,12 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
baseURL.RawQuery = query.Encode()
cid := p.config.Twitch.ClientID
auth := p.config.Twitch.Authorization
cid := p.config.Get("Twitch.ClientID", "")
auth := p.config.Get("Twitch.Authorization", "")
if cid == auth && cid == "" {
log.Info().Msgf("Twitch plugin not enabled.")
return
}
body, ok := getRequest(baseURL.String(), cid, auth)
if !ok {
@ -211,32 +224,75 @@ func (p *TwitchPlugin) checkTwitch(channel string, twitcher *Twitcher, alwaysPri
var s stream
err = json.Unmarshal(body, &s)
if err != nil {
log.Println(err)
log.Error().Err(err)
return
}
games := s.Data
game := ""
gameID, title := "", ""
if len(games) > 0 {
game = games[0].Title
gameID = games[0].GameID
title = games[0].Title
}
notStreamingTpl := p.config.Get("Twitch.NotTpl", notStreamingTplFallback)
isStreamingTpl := p.config.Get("Twitch.IsTpl", isStreamingTplFallback)
stoppedStreamingTpl := p.config.Get("Twitch.StoppedTpl", stoppedStreamingTplFallback)
buf := bytes.Buffer{}
info := struct {
Name string
Game string
URL string
}{
twitcher.name,
title,
twitcher.URL(),
}
if alwaysPrintStatus {
if game == "" {
p.Bot.SendMessage(channel, twitcher.name+" is not streaming.")
if gameID == "" {
t, err := template.New("notStreaming").Parse(notStreamingTpl)
if err != nil {
log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("notStreaming").Parse(notStreamingTplFallback))
}
t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String())
} else {
p.Bot.SendMessage(channel, twitcher.name+" is streaming "+game+" at "+twitcher.URL())
t, err := template.New("isStreaming").Parse(isStreamingTpl)
if err != nil {
log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
}
t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String())
}
} else if game == "" {
if twitcher.game != "" {
p.Bot.SendMessage(channel, twitcher.name+" just stopped streaming.")
} else if gameID == "" {
if twitcher.gameID != "" {
t, err := template.New("stoppedStreaming").Parse(stoppedStreamingTpl)
if err != nil {
log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("stoppedStreaming").Parse(stoppedStreamingTplFallback))
}
t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String())
}
twitcher.game = ""
twitcher.gameID = ""
} else {
if twitcher.game != game {
p.Bot.SendMessage(channel, twitcher.name+" just started streaming "+game+" at "+twitcher.URL())
if twitcher.gameID != gameID {
t, err := template.New("isStreaming").Parse(isStreamingTpl)
if err != nil {
log.Error().Err(err)
p.bot.Send(c, bot.Message, channel, err)
t = template.Must(template.New("isStreaming").Parse(isStreamingTplFallback))
}
t.Execute(&buf, info)
p.bot.Send(c, bot.Message, channel, buf.String())
}
twitcher.game = game
twitcher.gameID = gameID
}
}
func (p *TwitchPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

@ -17,52 +17,43 @@ type YourPlugin struct {
}
// NewYourPlugin creates a new YourPlugin with the Plugin interface
func New(bot bot.Bot) *YourPlugin {
return &YourPlugin{
bot: bot,
config: bot.Config(),
func New(b bot.Bot) *YourPlugin {
yp := &YourPlugin{
bot: b,
config: b.Config(),
}
b.Register(yp, bot.Message, yp.message)
b.Register(yp, bot.Help, yp.help)
return yp
}
// Message responds to the bot hook on recieving messages.
// This function returns true if the plugin responds in a meaningful way to the users message.
// Otherwise, the function returns false and the bot continues execution of other plugins.
func (p *YourPlugin) Message(message msg.Message) bool {
if len(message.Body) > p.config.Your.MaxLength {
func (p *YourPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
maxLen := p.config.GetInt("your.maxlength", 140)
if len(message.Body) > maxLen {
return false
}
msg := message.Body
for _, replacement := range p.config.Your.Replacements {
if rand.Float64() < replacement.Frequency {
r := strings.NewReplacer(replacement.This, replacement.That)
for _, replacement := range p.config.GetArray("Your.Replacements", []string{}) {
freq := p.config.GetFloat64("your.replacements."+replacement+".freq", 0.0)
this := p.config.Get("your.replacements."+replacement+".this", "")
that := p.config.Get("your.replacements."+replacement+".that", "")
if rand.Float64() < freq {
r := strings.NewReplacer(this, that)
msg = r.Replace(msg)
}
}
if msg != message.Body {
p.bot.SendMessage(message.Channel, msg)
p.bot.Send(c, bot.Message, message.Channel, msg)
return true
}
return false
}
// Help responds to help requests. Every plugin must implement a help function.
func (p *YourPlugin) Help(channel string, parts []string) {
p.bot.SendMessage(channel, "Your corrects people's grammar.")
func (p *YourPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
p.bot.Send(c, bot.Message, message.Channel, "Your corrects people's grammar.")
return true
}
// Empty event handler because this plugin does not do anything on event recv
func (p *YourPlugin) Event(kind string, message msg.Message) bool {
return false
}
// Handler for bot's own messages
func (p *YourPlugin) BotMessage(message msg.Message) bool {
return false
}
// Register any web URLs desired
func (p *YourPlugin) RegisterWeb() *string {
return nil
}
func (p *YourPlugin) ReplyMessage(message msg.Message, identifier string) bool { return false }

View File

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

View File

@ -8,12 +8,13 @@ import (
"bytes"
"go/build"
"io"
"log"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
)
@ -26,14 +27,17 @@ type ZorkPlugin struct {
zorks map[string]io.WriteCloser
}
func New(b bot.Bot) bot.Handler {
return &ZorkPlugin{
func New(b bot.Bot) bot.Plugin {
z := &ZorkPlugin{
bot: b,
zorks: make(map[string]io.WriteCloser),
}
b.Register(z, bot.Message, z.message)
b.Register(z, bot.Help, z.help)
return z
}
func (p *ZorkPlugin) runZork(ch string) error {
func (p *ZorkPlugin) runZork(c bot.Connector, ch string) error {
const importString = "github.com/velour/catbase/plugins/zork"
pkg, err := build.Import(importString, "", build.FindOnly)
if err != nil {
@ -49,7 +53,7 @@ func (p *ZorkPlugin) runZork(ch string) error {
var w io.WriteCloser
cmd.Stdin, w = io.Pipe()
log.Printf("zork running %v\n", cmd)
log.Info().Msgf("zork running %v", cmd)
if err := cmd.Start(); err != nil {
w.Close()
return err
@ -75,25 +79,25 @@ func (p *ZorkPlugin) runZork(ch string) error {
m := strings.Replace(s.Text(), ">", "", -1)
m = strings.Replace(m, "\n", "\n>", -1)
m = ">" + m + "\n"
p.bot.SendMessage(ch, m)
p.bot.Send(c, bot.Message, ch, m)
}
}()
go func() {
if err := cmd.Wait(); err != nil {
log.Printf("zork exited: %v\n", err)
log.Error().Err(err).Msg("zork exited")
}
p.Lock()
p.zorks[ch] = nil
p.Unlock()
}()
log.Printf("zork is running in %s\n", ch)
log.Info().Msgf("zork is running in %s\n", ch)
p.zorks[ch] = w
return nil
}
func (p *ZorkPlugin) Message(message msg.Message) bool {
func (p *ZorkPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
m := strings.ToLower(message.Body)
log.Printf("got message [%s]\n", m)
log.Debug().Msgf("got message [%s]", m)
if ts := strings.Fields(m); len(ts) < 1 || ts[0] != "zork" {
return false
}
@ -103,24 +107,17 @@ func (p *ZorkPlugin) Message(message msg.Message) bool {
p.Lock()
defer p.Unlock()
if p.zorks[ch] == nil {
if err := p.runZork(ch); err != nil {
p.bot.SendMessage(ch, "failed to run zork: "+err.Error())
if err := p.runZork(c, ch); err != nil {
p.bot.Send(c, bot.Message, ch, "failed to run zork: "+err.Error())
return true
}
}
log.Printf("zorking, [%s]\n", m)
log.Debug().Msgf("zorking, [%s]", m)
io.WriteString(p.zorks[ch], m+"\n")
return true
}
func (p *ZorkPlugin) Event(_ string, _ msg.Message) bool { return false }
func (p *ZorkPlugin) BotMessage(_ msg.Message) bool { return false }
func (p *ZorkPlugin) Help(ch string, _ []string) {
p.bot.SendMessage(ch, "Play zork using 'zork <zork command>'.")
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>'.")
return true
}
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"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var (
@ -20,9 +22,10 @@ var (
func main() {
flag.Parse()
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
if *token == "" {
log.Printf("No token provided.")
log.Fatal().Msg("No token provided.")
return
}
@ -35,7 +38,7 @@ func main() {
func getFiles() map[string]string {
files := fileResp{}
log.Printf("Getting files")
log.Debug().Msgf("Getting files")
body := mkReq("https://slack.com/api/emoji.list",
"token", *token,
)
@ -43,9 +46,9 @@ func getFiles() map[string]string {
err := json.Unmarshal(body, &files)
checkErr(err)
log.Printf("Ok: %v", files.Ok)
log.Debug().Msgf("Ok: %v", files.Ok)
if !files.Ok {
log.Println(files)
log.Debug().Msgf("%+v", files)
}
return files.Files
@ -55,7 +58,7 @@ func downloadFile(n, f string) {
url := strings.Replace(f, "\\", "", -1) // because fuck slack
if strings.HasPrefix(url, "alias:") {
log.Printf("Skipping alias: %s", url)
log.Debug().Msgf("Skipping alias: %s", url)
return
}
@ -66,7 +69,7 @@ func downloadFile(n, f string) {
fname := filepath.Join(*path, n+"."+ext)
log.Printf("Downloading from: %s", url)
log.Debug().Msgf("Downloading from: %s", url)
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
@ -82,18 +85,18 @@ func downloadFile(n, f string) {
defer out.Close()
io.Copy(out, resp.Body)
log.Printf("Downloaded %s", f)
log.Debug().Msgf("Downloaded %s", f)
}
func checkErr(err error) {
if err != nil {
log.Fatal(err)
log.Fatal().Err(err)
}
}
func mkReq(path string, arg ...string) []byte {
if len(arg)%2 != 0 {
log.Fatal("Bad request arg number.")
log.Fatal().Msg("Bad request arg number.")
}
u, err := url.Parse(path)

View File

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

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"