catbase/plugins/counter/counter.go

631 lines
16 KiB
Go
Raw Normal View History

2016-03-19 15:44:27 +00:00
package counter
2013-01-23 21:25:04 +00:00
import (
"database/sql"
2019-05-27 18:27:34 +00:00
"encoding/json"
2013-01-23 21:25:04 +00:00
"fmt"
"html/template"
2019-02-12 16:03:24 +00:00
"math/rand"
2019-05-27 18:27:34 +00:00
"net/http"
"regexp"
2017-01-23 15:10:54 +00:00
"strconv"
2013-01-23 21:25:04 +00:00
"strings"
2019-03-07 16:35:42 +00:00
"github.com/rs/zerolog/log"
2016-03-19 18:02:46 +00:00
"github.com/jmoiron/sqlx"
2016-01-17 18:00:44 +00:00
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
2013-01-23 21:25:04 +00:00
)
// This is a counter plugin to count arbitrary things.
var teaMatcher = regexp.MustCompile("(?i)^([^.]+)\\. [^.]*\\. ([^.]*\\.?)+$")
2013-01-23 21:25:04 +00:00
type CounterPlugin struct {
Bot bot.Bot
2016-03-19 18:02:46 +00:00
DB *sqlx.DB
2013-01-23 21:25:04 +00:00
}
type Item struct {
2016-03-19 18:02:46 +00:00
*sqlx.DB
ID int64
2013-01-23 21:25:04 +00:00
Nick string
Item string
Count int
}
2018-01-04 17:23:59 +00:00
type alias struct {
*sqlx.DB
ID int64
Item string
PointsTo string `db:"points_to"`
}
2019-05-27 18:27:34 +00:00
// 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
}
2016-03-19 18:27:02 +00:00
// GetItems returns all counters for a subject
2016-03-19 18:02:46 +00:00
func GetItems(db *sqlx.DB, nick string) ([]Item, error) {
var items []Item
err := db.Select(&items, `select * from counter where nick = ?`, nick)
if err != nil {
return nil, err
}
// Don't forget to embed the DB into all of that shiz
2017-01-24 02:13:21 +00:00
for i := range items {
items[i].DB = db
2016-03-19 18:02:46 +00:00
}
return items, nil
}
2018-01-04 12:39:24 +00:00
func LeaderAll(db *sqlx.DB) ([]Item, error) {
s := `select id,item,nick,max(count) as count from counter group by item having count(nick) > 1 and max(count) > 1 order by count desc`
var items []Item
err := db.Select(&items, s)
if err != nil {
return nil, err
}
for i := range items {
items[i].DB = db
}
return items, nil
}
func Leader(db *sqlx.DB, itemName string) ([]Item, error) {
2018-01-04 17:23:59 +00:00
itemName = strings.ToLower(itemName)
2018-01-04 12:39:24 +00:00
s := `select * from counter where item=? order by count desc`
var items []Item
err := db.Select(&items, s, itemName)
if err != nil {
return nil, err
}
for i := range items {
items[i].DB = db
}
return items, nil
}
2018-01-04 17:23:59 +00:00
func MkAlias(db *sqlx.DB, item, pointsTo string) (*alias, error) {
item = strings.ToLower(item)
pointsTo = strings.ToLower(pointsTo)
res, err := db.Exec(`insert into counter_alias (item, points_to) values (?, ?)`,
item, pointsTo)
if err != nil {
_, err := db.Exec(`update counter_alias set points_to=? where item=?`, pointsTo, item)
if err != nil {
return nil, err
}
var a alias
if err := db.Get(&a, `select * from counter_alias where item=?`, item); err != nil {
return nil, err
}
return &a, nil
}
id, _ := res.LastInsertId()
return &alias{db, id, item, pointsTo}, nil
}
2016-03-19 18:27:02 +00:00
// GetItem returns a specific counter for a subject
2016-03-19 18:02:46 +00:00
func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) {
2019-11-25 19:26:26 +00:00
itemName = trimUnicode(itemName)
2016-03-19 18:02:46 +00:00
var item Item
item.DB = db
2018-01-04 17:23:59 +00:00
var a alias
if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil {
itemName = a.PointsTo
} else {
2019-03-07 16:35:42 +00:00
log.Error().Err(err).Interface("alias", a)
2018-01-04 17:23:59 +00:00
}
2016-03-19 18:02:46 +00:00
err := db.Get(&item, `select * from counter where nick = ? and item= ?`,
nick, itemName)
switch err {
case sql.ErrNoRows:
item.ID = -1
item.Nick = nick
item.Item = itemName
case nil:
default:
return Item{}, err
}
2019-03-07 16:35:42 +00:00
log.Debug().
Str("nick", nick).
Str("itemName", itemName).
Interface("item", item).
Msg("got item")
2016-03-19 18:02:46 +00:00
return item, nil
}
2016-03-19 18:27:02 +00:00
// Create saves a counter
2016-03-19 18:02:46 +00:00
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
}
2016-03-19 18:02:46 +00:00
id, _ := res.LastInsertId()
// hackhackhack?
i.ID = id
return err
}
2016-03-19 18:27:02 +00:00
// UpdateDelta sets a value
// This will create or delete the item if necessary
func (i *Item) Update(value int) error {
i.Count = value
2016-03-19 18:02:46 +00:00
if i.Count == 0 && i.ID != -1 {
return i.Delete()
}
if i.ID == -1 {
i.Create()
}
2019-03-07 16:35:42 +00:00
log.Debug().
Interface("i", i).
Int("value", value).
Msg("Updating item")
2016-03-19 18:02:46 +00:00
_, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID)
return err
}
2016-03-19 18:27:02 +00:00
// UpdateDelta changes a value according to some delta
// This will create or delete the item if necessary
func (i *Item) UpdateDelta(delta int) error {
i.Count += delta
return i.Update(i.Count)
}
// Delete removes a counter from the database
2016-03-19 18:02:46 +00:00
func (i *Item) Delete() error {
_, err := i.Exec(`delete from counter where id = ?`, i.ID)
i.ID = -1
return err
}
2013-01-23 21:25:04 +00:00
// NewCounterPlugin creates a new CounterPlugin with the Plugin interface
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
);`)
b.DB().MustExec(`create table if not exists counter_alias (
2018-01-04 17:23:59 +00:00
id integer PRIMARY KEY AUTOINCREMENT,
item string NOT NULL UNIQUE,
points_to string NOT NULL
);`)
tx.Commit()
cp := &CounterPlugin{
Bot: b,
DB: b.DB(),
2013-01-23 21:25:04 +00:00
}
b.Register(cp, bot.Message, cp.message)
b.Register(cp, bot.Help, cp.help)
2019-05-27 18:27:34 +00:00
cp.registerWeb()
return cp
2013-01-23 21:25:04 +00:00
}
2019-11-25 19:26:26 +00:00
func trimUnicode(s string) string {
return strings.Trim(s, string(rune(0xFE0F)))
}
2013-01-23 21:25:04 +00:00
// 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
2013-01-24 15:20:15 +00:00
// execution of other plugins.
2019-05-27 23:21:53 +00:00
func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
2013-01-23 21:25:04 +00:00
// This bot does not reply to anything
nick := message.User.Name
channel := message.Channel
parts := strings.Fields(message.Body)
2013-01-23 21:25:04 +00:00
if len(parts) == 0 {
return false
}
2018-01-04 17:23:59 +00:00
if len(parts) == 3 && strings.ToLower(parts[0]) == "mkalias" {
if _, err := MkAlias(p.DB, parts[1], parts[2]); err != nil {
2019-03-07 16:35:42 +00:00
log.Error().Err(err)
2018-01-04 17:23:59 +00:00
return false
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("Created alias %s -> %s",
2018-01-04 17:23:59 +00:00
parts[1], parts[2]))
return true
} else if strings.ToLower(parts[0]) == "leaderboard" {
2018-01-04 12:39:24 +00:00
var cmd func() ([]Item, error)
itNameTxt := ""
if len(parts) == 1 {
cmd = func() ([]Item, error) { return LeaderAll(p.DB) }
} else {
itNameTxt = fmt.Sprintf(" for %s", parts[1])
cmd = func() ([]Item, error) { return Leader(p.DB, parts[1]) }
}
its, err := cmd()
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().Err(err)
2018-01-04 12:39:24 +00:00
return false
} else if len(its) == 0 {
return false
}
out := fmt.Sprintf("Leaderboard%s:\n", itNameTxt)
for _, it := range its {
out += fmt.Sprintf("%s with %d %s\n",
it.Nick,
it.Count,
it.Item,
)
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, out)
2018-01-04 12:39:24 +00:00
return true
} else if match := teaMatcher.MatchString(message.Body); match {
// check for tea match TTT
2019-05-27 23:21:53 +00:00
return p.checkMatch(c, message)
2017-01-31 20:35:05 +00:00
} else if message.Command && message.Body == "reset me" {
2017-01-24 02:13:21 +00:00
items, err := GetItems(p.DB, strings.ToLower(nick))
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("nick", nick).
Msg("Error getting items to reset")
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, "Something is technically wrong with your counters.")
2017-01-24 02:13:21 +00:00
return true
}
2019-03-07 16:35:42 +00:00
log.Debug().Msgf("Items: %+v", items)
2017-01-24 02:13:21 +00:00
for _, item := range items {
item.Delete()
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s, you are as new, my son.", nick))
2017-01-24 02:13:21 +00:00
return true
} else if message.Command && parts[0] == "inspect" && len(parts) == 2 {
var subject string
if parts[1] == "me" {
subject = strings.ToLower(nick)
} else {
2016-03-19 18:02:46 +00:00
subject = strings.ToLower(parts[1])
}
2019-03-07 16:35:42 +00:00
log.Debug().
Str("subject", subject).
Msg("Getting counter")
// pull all of the items associated with "subject"
2016-03-19 18:02:46 +00:00
items, err := GetItems(p.DB, subject)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Msg("Error retrieving items")
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, "Something went wrong finding that counter;")
2016-03-19 18:02:46 +00:00
return true
}
resp := fmt.Sprintf("%s has the following counters:", subject)
count := 0
2016-03-19 18:02:46 +00:00
for _, it := range items {
count += 1
if count > 1 {
resp += ","
}
2016-03-19 18:02:46 +00:00
resp += fmt.Sprintf(" %s: %d", it.Item, it.Count)
if count > 20 {
2016-03-19 18:02:46 +00:00
resp += ", and a few others"
break
}
}
2016-03-19 18:02:46 +00:00
resp += "."
if count == 0 {
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has no counters.", subject))
return true
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, resp)
return true
} else if message.Command && len(parts) == 2 && parts[0] == "clear" {
subject := strings.ToLower(nick)
itemName := strings.ToLower(parts[1])
2016-03-19 18:02:46 +00:00
it, err := GetItem(p.DB, subject, itemName)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error getting item to remove")
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
2016-03-19 18:02:46 +00:00
return true
}
err = it.Delete()
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error removing item")
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, "Something went wrong removing that counter;")
return true
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Action, channel, fmt.Sprintf("chops a few %s out of his brain",
2013-01-24 15:20:15 +00:00
itemName))
return true
} else if message.Command && parts[0] == "count" {
2013-01-23 21:25:04 +00:00
var subject string
var itemName string
if len(parts) == 3 {
// report count for parts[1]
subject = strings.ToLower(parts[1])
itemName = strings.ToLower(parts[2])
} else if len(parts) == 2 {
subject = strings.ToLower(nick)
itemName = strings.ToLower(parts[1])
} else {
return false
}
var item Item
2016-03-19 18:02:46 +00:00
item, err := GetItem(p.DB, subject, itemName)
switch {
case err == sql.ErrNoRows:
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("I don't think %s has any %s.",
2013-01-23 21:25:04 +00:00
subject, itemName))
return true
case err != nil:
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error retrieving item count")
return true
2013-01-23 21:25:04 +00:00
}
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject, item.Count,
2013-01-24 15:20:15 +00:00
itemName))
2013-01-23 21:25:04 +00:00
return true
} else if len(parts) <= 2 {
if (len(parts) == 2) && (parts[1] == "++" || parts[1] == "--") {
parts = []string{parts[0] + parts[1]}
}
2016-03-19 18:02:46 +00:00
// Need to have at least 3 characters to ++ or --
if len(parts[0]) < 3 {
return false
}
2013-01-23 21:25:04 +00:00
subject := strings.ToLower(nick)
itemName := strings.ToLower(parts[0])[:len(parts[0])-2]
2013-01-24 15:35:39 +00:00
if nameParts := strings.SplitN(itemName, ".", 2); len(nameParts) == 2 {
subject = nameParts[0]
itemName = nameParts[1]
}
2013-01-23 21:25:04 +00:00
if strings.HasSuffix(parts[0], "++") {
// ++ those fuckers
2016-03-19 18:02:46 +00:00
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("error finding item")
2016-03-19 18:02:46 +00:00
// Item ain't there, I guess
return false
}
2019-03-07 16:35:42 +00:00
log.Debug().Msgf("About to update item: %#v", item)
2016-03-19 18:27:02 +00:00
item.UpdateDelta(1)
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
2013-01-24 15:35:39 +00:00
item.Count, item.Item))
2013-01-23 21:25:04 +00:00
return true
} else if strings.HasSuffix(parts[0], "--") {
// -- those fuckers
2016-03-19 18:02:46 +00:00
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
2016-03-19 18:02:46 +00:00
// Item ain't there, I guess
return false
}
2016-03-19 18:27:02 +00:00
item.UpdateDelta(-1)
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
2013-01-24 15:35:39 +00:00
item.Count, item.Item))
2013-01-23 21:25:04 +00:00
return true
}
2017-01-17 22:57:39 +00:00
} else if len(parts) == 3 {
// Need to have at least 3 characters to ++ or --
if len(parts[0]) < 3 {
return false
}
subject := strings.ToLower(nick)
2017-01-23 15:10:54 +00:00
itemName := strings.ToLower(parts[0])
2017-01-17 22:57:39 +00:00
if nameParts := strings.SplitN(itemName, ".", 2); len(nameParts) == 2 {
subject = nameParts[0]
itemName = nameParts[1]
}
if parts[1] == "+=" {
// += those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
2017-01-17 22:57:39 +00:00
// Item ain't there, I guess
return false
}
2017-01-23 15:10:54 +00:00
n, _ := strconv.Atoi(parts[2])
2019-03-07 16:35:42 +00:00
log.Debug().Msgf("About to update item by %d: %#v", n, item)
2017-01-17 22:57:39 +00:00
item.UpdateDelta(n)
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
2017-01-17 22:57:39 +00:00
item.Count, item.Item))
return true
} else if parts[1] == "-=" {
// -= those fuckers
item, err := GetItem(p.DB, subject, itemName)
if err != nil {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("subject", subject).
Str("itemName", itemName).
Msg("Error finding item")
2017-01-17 22:57:39 +00:00
// Item ain't there, I guess
return false
}
2017-01-23 15:10:54 +00:00
n, _ := strconv.Atoi(parts[2])
2019-03-07 16:35:42 +00:00
log.Debug().Msgf("About to update item by -%d: %#v", n, item)
2017-01-17 22:57:39 +00:00
item.UpdateDelta(-n)
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s has %d %s.", subject,
2017-01-17 22:57:39 +00:00
item.Count, item.Item))
return true
}
2013-01-23 21:25:04 +00:00
}
return false
}
// Help responds to help requests. Every plugin must implement a help function.
2019-05-27 23:21:53 +00:00
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 "+
2013-01-24 15:35:39 +00:00
"<noun>++ and <noun>--. You can see all of your counters using "+
"\"inspect\", erase them with \"clear\", and view single counters with "+
"\"count\".")
return true
}
2013-06-01 17:10:15 +00:00
2019-05-27 23:21:53 +00:00
func (p *CounterPlugin) checkMatch(c bot.Connector, message msg.Message) bool {
nick := message.User.Name
channel := message.Channel
submatches := teaMatcher.FindStringSubmatch(message.Body)
if len(submatches) <= 1 {
return false
}
itemName := strings.ToLower(submatches[1])
// We will specifically allow :tea: to keep compatability
item, err := GetItem(p.DB, nick, itemName)
if err != nil || (item.Count == 0 && item.Item != ":tea:") {
2019-03-07 16:35:42 +00:00
log.Error().
Err(err).
Str("itemName", itemName).
Msg("Error finding item")
// Item ain't there, I guess
return false
}
2019-03-07 16:35:42 +00:00
log.Debug().Msgf("About to update item: %#v", item)
item.UpdateDelta(1)
2019-05-27 23:21:53 +00:00
p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("%s... %s has %d %s",
2019-02-12 16:03:24 +00:00
strings.Join(everyDayImShuffling([]string{"bleep", "bloop", "blop"}), "-"), nick, item.Count, itemName))
return true
}
2019-02-12 16:03:24 +00:00
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
}
2019-05-27 18:27:34 +00:00
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))
2019-05-27 18:27:34 +00:00
func (p *CounterPlugin) handleCounter(w http.ResponseWriter, r *http.Request) {
tpl.Execute(w, struct{ Nav []bot.EndPoint }{p.Bot.GetWebNavigation()})
2019-05-27 18:27:34 +00:00
}
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
2019-05-27 18:27:34 +00:00
}{}
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
}
2019-05-27 18:27:34 +00:00
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))
}