catbase/plugins/goals/goals.go

341 lines
9.5 KiB
Go
Raw Normal View History

2020-05-25 18:05:21 +00:00
package goals
import (
"fmt"
2021-12-20 17:40:10 +00:00
bh "github.com/timshannon/bolthold"
2020-05-25 18:05:21 +00:00
"regexp"
2020-05-26 15:38:55 +00:00
"sort"
2020-05-25 18:05:21 +00:00
"strconv"
2020-07-13 15:34:53 +00:00
"time"
2020-05-25 18:05:21 +00:00
"github.com/rs/zerolog/log"
"github.com/velour/catbase/bot"
"github.com/velour/catbase/bot/msg"
"github.com/velour/catbase/config"
"github.com/velour/catbase/plugins/counter"
)
type GoalsPlugin struct {
2021-02-07 18:29:02 +00:00
b bot.Bot
cfg *config.Config
2021-12-20 17:40:10 +00:00
store *bh.Store
2021-02-07 18:29:02 +00:00
handlers bot.HandlerTable
2020-05-25 18:05:21 +00:00
}
func New(b bot.Bot) *GoalsPlugin {
p := &GoalsPlugin{
2021-12-20 17:40:10 +00:00
b: b,
cfg: b.Config(),
store: b.Store(),
2020-05-25 18:05:21 +00:00
}
2021-02-07 18:29:02 +00:00
p.registerCmds()
2020-05-25 18:05:21 +00:00
b.Register(p, bot.Help, p.help)
counter.RegisterUpdate(p.update)
return p
}
2021-02-07 18:29:02 +00:00
func (p *GoalsPlugin) registerCmds() {
p.handlers = bot.HandlerTable{
{Kind: bot.Message, IsCmd: true,
2021-08-23 15:28:08 +00:00
Regex: regexp.MustCompile(`(?i)^register (?P<type>competition|goal) for (?P<who>[[:punct:][:alnum:]]+) (?P<what>[^\s]+) (?P<amount>[[:digit:]]+)?`),
2021-02-07 18:29:02 +00:00
HelpText: "Register with `%s` for other people",
Handler: func(r bot.Request) bool {
2021-08-23 15:28:08 +00:00
log.Debug().Interface("values", r.Values).Msg("trying to register a goal")
2021-02-07 18:29:02 +00:00
amount, _ := strconv.Atoi(r.Values["amount"])
p.register(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Values["who"], amount)
return true
}},
{Kind: bot.Message, IsCmd: true,
2021-08-23 15:28:08 +00:00
Regex: regexp.MustCompile(`(?i)^register (?P<type>competition|goal) (?P<what>[^\s]+) (?P<amount>[[:digit:]]+)?`),
2021-05-04 18:05:21 +00:00
HelpText: "Register with `%s` for yourself",
2021-02-07 18:29:02 +00:00
Handler: func(r bot.Request) bool {
2021-05-04 18:05:21 +00:00
amount, _ := strconv.Atoi(r.Values["amount"])
p.register(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Msg.User.Name, amount)
2021-02-07 18:29:02 +00:00
return true
}},
{Kind: bot.Message, IsCmd: true,
Regex: regexp.MustCompile(`(?i)^deregister (?P<type>competition|goal) for (?P<who>[[:punct:][:alnum:]]+) (?P<what>.*)`),
HelpText: "Deregister with `%s` for other people",
Handler: func(r bot.Request) bool {
p.deregister(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Values["who"])
return true
}},
{Kind: bot.Message, IsCmd: true,
2021-05-04 18:05:21 +00:00
Regex: regexp.MustCompile(`(?i)^deregister (?P<type>competition|goal) (?P<what>[[:punct:][:alnum:]]+)`),
HelpText: "Deregister with `%s` for yourself",
2021-02-07 18:29:02 +00:00
Handler: func(r bot.Request) bool {
2021-05-04 18:05:21 +00:00
p.deregister(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Msg.User.Name)
2021-02-07 18:29:02 +00:00
return true
}},
{Kind: bot.Message, IsCmd: true,
2021-08-23 15:31:57 +00:00
Regex: regexp.MustCompile(`(?i)^check (?P<type>competition|goal) for (?P<who>[[:punct:][:alnum:]]+) (?P<what>[^\s]+)`),
2021-02-07 18:29:02 +00:00
HelpText: "Check with `%s` for other people",
Handler: func(r bot.Request) bool {
p.check(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Values["who"])
return true
}},
2021-05-04 18:05:21 +00:00
{Kind: bot.Message, IsCmd: true,
2021-08-23 15:31:57 +00:00
Regex: regexp.MustCompile(`(?i)^check (?P<type>competition|goal) (?P<what>[^\s]+)`),
2021-05-04 18:05:21 +00:00
HelpText: "Check with `%s` for yourself",
Handler: func(r bot.Request) bool {
p.check(r.Conn, r.Msg.Channel, r.Values["type"], r.Values["what"], r.Msg.User.Name)
return true
}},
2020-05-25 18:05:21 +00:00
}
2021-02-07 18:29:02 +00:00
p.b.RegisterTable(p, p.handlers)
2020-05-25 18:05:21 +00:00
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) register(c bot.Connector, ch, kind, what, who string, howMuch int) {
2020-05-25 18:05:21 +00:00
if kind == "goal" && howMuch == 0 {
p.b.Send(c, bot.Message, ch,
fmt.Sprintf("%s, you need to have a goal amount if you want to have a goal for %s.", who, what))
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
g := p.newGoal(kind, who, what, howMuch)
err := g.Save()
if err != nil {
log.Error().Err(err).Msgf("could not create goal")
p.b.Send(c, bot.Message, ch, "I couldn't create that goal for some reason.")
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
p.b.Send(c, bot.Message, ch, fmt.Sprintf("%s created", kind))
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) deregister(c bot.Connector, ch, kind, what, who string) {
g, err := p.getGoalKind(kind, who, what)
2020-05-25 18:05:21 +00:00
if err != nil {
log.Error().Err(err).Msgf("could not find goal to delete")
p.b.Send(c, bot.Message, ch, "I couldn't find that item to deregister.")
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
err = g.Delete()
if err != nil {
log.Error().Err(err).Msgf("could not delete goal")
p.b.Send(c, bot.Message, ch, "I couldn't deregister that.")
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
p.b.Send(c, bot.Message, ch, fmt.Sprintf("%s %s deregistered", kind, what))
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) check(c bot.Connector, ch, kind, what, who string) {
2021-08-23 15:28:08 +00:00
log.Debug().Msgf("checking goal in channel %s", ch)
2020-05-26 15:38:55 +00:00
if kind == "goal" {
p.checkGoal(c, ch, what, who)
return
}
p.checkCompetition(c, ch, what, who)
2020-05-25 18:05:21 +00:00
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) checkCompetition(c bot.Connector, ch, what, who string) {
2021-12-20 17:40:10 +00:00
items, err := counter.GetItem(p.store, what)
2020-05-26 15:38:55 +00:00
if err != nil || len(items) == 0 {
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what))
return
}
sort.Slice(items, func(i, j int) bool {
if items[i].Count == items[j].Count && who == items[i].Nick {
return true
}
if items[i].Count > items[j].Count {
return true
}
return false
})
if items[0].Nick == who && len(items) > 1 && items[1].Count == items[0].Count {
p.b.Send(c, bot.Message, ch,
fmt.Sprintf("Congratulations! You're in the lead for %s with %d, but you're tied with %s",
what, items[0].Count, items[1].Nick))
return
}
if items[0].Nick == who {
p.b.Send(c, bot.Message, ch, fmt.Sprintf("Congratulations! You're in the lead for %s with %d.",
what, items[0].Count))
return
}
count := 0
for _, i := range items {
if i.Nick == who {
count = i.Count
}
}
p.b.Send(c, bot.Message, ch, fmt.Sprintf("%s is in the lead for %s with %d. You have %d to catch up.",
items[0].Nick, what, items[0].Count, items[0].Count-count))
2020-05-25 18:05:21 +00:00
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) checkGoal(c bot.Connector, ch, what, who string) {
g, err := p.getGoalKind("goal", who, what)
2020-05-25 18:05:21 +00:00
if err != nil {
2020-05-26 15:38:55 +00:00
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find %s", what))
2020-05-25 18:05:21 +00:00
}
nick, id := "", ""
user, err := c.Profile(who)
if err == nil && user.ID != "" {
id = user.ID
nick = user.Name
2021-08-23 15:28:08 +00:00
} else {
log.Error().Err(err).Msg("no user returned for goal check")
}
2021-08-23 15:28:08 +00:00
log.Debug().
Str("nick", nick).
Str("id", id).
Str("what", what).
Msg("looking for item")
2021-12-20 17:40:10 +00:00
item, err := counter.GetUserItem(p.store, nick, id, what)
2020-05-25 18:05:21 +00:00
if err != nil {
p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what))
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
perc := float64(item.Count) / float64(g.Amount) * 100.0
2020-07-13 15:34:53 +00:00
remaining := p.remainingText(item, g)
2020-05-25 18:05:21 +00:00
log.Debug().Msgf("ch: %v, perc: %.2f, remaining: %s", ch, perc, remaining)
2020-05-26 15:38:55 +00:00
if perc >= 100 {
p.deregister(c, ch, g.Kind, g.What, g.Who)
m := fmt.Sprintf("You made it! You have %.2f%% of %s and now it's done.", perc, what)
p.b.Send(c, bot.Message, ch, m)
} else {
2020-07-13 15:34:53 +00:00
m := fmt.Sprintf("You have %d out of %d for %s. You're %.2f%% of the way there! %s",
item.Count, g.Amount, what, perc, remaining)
2020-05-26 15:38:55 +00:00
p.b.Send(c, bot.Message, ch, m)
}
2020-05-25 18:05:21 +00:00
2020-05-26 15:38:55 +00:00
return
2020-05-25 18:05:21 +00:00
}
func (p *GoalsPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool {
ch := message.Channel
msg := "Goals can set goals and competition for your counters."
2021-02-07 18:29:02 +00:00
for _, cmd := range p.handlers {
msg += fmt.Sprintf("\n"+cmd.HelpText, cmd.Regex)
}
2020-05-25 18:05:21 +00:00
p.b.Send(c, bot.Message, ch, msg)
return true
}
type cmd map[string]string
func parseCmd(r *regexp.Regexp, body string) cmd {
out := cmd{}
subs := r.FindStringSubmatch(body)
if len(subs) == 0 {
return out
}
for i, n := range r.SubexpNames() {
out[n] = subs[i]
}
return out
}
type goal struct {
2021-12-20 17:40:10 +00:00
ID int64 `boltholdid:"ID"`
2020-05-25 18:05:21 +00:00
Kind string
Who string
What string
Amount int
gp *GoalsPlugin
}
func (p *GoalsPlugin) newGoal(kind, who, what string, amount int) goal {
return goal{
ID: -1,
Kind: kind,
Who: who,
What: what,
Amount: amount,
gp: p,
}
}
2020-05-26 15:38:55 +00:00
func (p *GoalsPlugin) getGoal(who, what string) ([]*goal, error) {
gs := []*goal{}
2021-12-20 17:40:10 +00:00
err := p.store.Find(&gs, bh.Where("who").Eq(who).And("what").Eq(what))
2020-05-26 15:38:55 +00:00
if err != nil {
return nil, err
}
for _, g := range gs {
g.gp = p
}
return gs, nil
}
func (p *GoalsPlugin) getGoalKind(kind, who, what string) (*goal, error) {
2020-05-25 18:05:21 +00:00
g := &goal{gp: p}
2021-12-20 17:40:10 +00:00
err := p.store.FindOne(&g, bh.Where("kind").Eq(kind).And("who").Eq(who).And("what").Eq(what))
2020-05-25 18:05:21 +00:00
if err != nil {
return nil, err
}
return g, nil
}
func (g *goal) Save() error {
2021-12-20 17:40:10 +00:00
err := g.gp.store.Insert(bh.NextSequence(), &g)
2020-05-25 18:05:21 +00:00
if err != nil {
return err
}
return nil
}
func (g goal) Delete() error {
if g.ID == -1 {
return nil
}
2021-12-20 17:40:10 +00:00
err := g.gp.store.Delete(goal{}, g.ID)
2020-05-25 18:05:21 +00:00
return err
}
2021-06-17 17:59:29 +00:00
func (p *GoalsPlugin) update(r bot.Request, u counter.Update) {
log.Debug().Msgf("entered update for %#v in ch: %v", u, r.Msg.Channel)
2020-05-26 15:38:55 +00:00
gs, err := p.getGoal(u.Who, u.What)
2020-05-25 18:05:21 +00:00
if err != nil {
log.Error().Err(err).Msgf("could not get goal for %#v", u)
return
}
c := p.b.DefaultConnector()
2020-05-26 15:38:55 +00:00
for _, g := range gs {
if g.Kind == "goal" {
p.checkGoal(c, r.Msg.Channel, u.What, u.Who)
} else {
p.checkCompetition(c, r.Msg.Channel, u.What, u.Who)
2020-05-26 15:38:55 +00:00
}
2020-05-25 18:05:21 +00:00
}
}
2020-07-13 15:34:53 +00:00
var now = time.Now
func (p *GoalsPlugin) calculateRemaining(i counter.Item, g *goal) int {
today := float64(now().YearDay())
thisYear := time.Date(now().Year(), 0, 0, 0, 0, 0, 0, time.UTC)
nextYear := time.Date(now().Year()+1, 0, 0, 0, 0, 0, 0, time.UTC)
days := nextYear.Sub(thisYear).Hours() / 24.0 // hopefully either a leap year on not
perc := today / days
shouldHave := float64(g.Amount) * perc
diff := int(shouldHave) - i.Count
log.Printf("Today is the %f-th day with %f days in the year", today, days)
return diff
}
func (p *GoalsPlugin) remainingText(i counter.Item, g *goal) string {
remaining := p.calculateRemaining(i, g)
txt := ""
if remaining < 0 {
txt = fmt.Sprintf("You're ahead by %d!", -1*remaining)
} else if remaining > 0 {
txt = fmt.Sprintf("You need %d to get back on track.", remaining)
}
return txt
}