diff --git a/main.go b/main.go index e04f165..7e75437 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/velour/catbase/plugins/achievements" "github.com/velour/catbase/plugins/aoc" + "github.com/velour/catbase/plugins/counter/goals" "github.com/velour/catbase/plugins/meme" "github.com/velour/catbase/plugins/sms" "github.com/velour/catbase/plugins/twitter" @@ -121,6 +122,7 @@ func main() { b.AddPlugin(remember.New(b)) b.AddPlugin(your.New(b)) b.AddPlugin(counter.New(b)) + b.AddPlugin(goals.New(b)) b.AddPlugin(reminder.New(b)) b.AddPlugin(babbler.New(b)) b.AddPlugin(zork.New(b)) diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 18d6fa7..7f80b18 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -14,6 +14,7 @@ import ( "github.com/rs/zerolog/log" "github.com/jmoiron/sqlx" + "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" ) @@ -197,6 +198,9 @@ func (i *Item) Update(value int) error { Int("value", value). Msg("Updating item") _, err := i.Exec(`update counter set count = ? where id = ?`, i.Count, i.ID) + if err == nil { + sendUpdate(i.Nick, i.Item, i.Count) + } return err } @@ -236,6 +240,7 @@ func New(b bot.Bot) *CounterPlugin { b.Register(cp, bot.Message, cp.message) b.Register(cp, bot.Help, cp.help) cp.registerWeb() + return cp } @@ -661,3 +666,24 @@ func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) } fmt.Fprint(w, string(data)) } + +type Update struct { + Who string + What string + Amount int +} + +type updateFunc func(Update) + +var updateFuncs = []updateFunc{} + +func RegisterUpdate(f updateFunc) { + log.Debug().Msgf("registering update func") + updateFuncs = append(updateFuncs, f) +} +func sendUpdate(who, what string, amount int) { + log.Debug().Msgf("sending updates to %d places", len(updateFuncs)) + for _, f := range updateFuncs { + f(Update{who, what, amount}) + } +} diff --git a/plugins/counter/goals/goals.go b/plugins/counter/goals/goals.go new file mode 100644 index 0000000..7c23a32 --- /dev/null +++ b/plugins/counter/goals/goals.go @@ -0,0 +1,251 @@ +package goals + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/jmoiron/sqlx" + "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 { + b bot.Bot + cfg *config.Config + db *sqlx.DB +} + +func New(b bot.Bot) *GoalsPlugin { + p := &GoalsPlugin{ + b: b, + cfg: b.Config(), + db: b.DB(), + } + p.mkDB() + b.Register(p, bot.Message, p.message) + b.Register(p, bot.Help, p.help) + counter.RegisterUpdate(p.update) + return p +} + +func (p *GoalsPlugin) mkDB() { + _, err := p.db.Exec(`create table if not exists goals ( + id integer primary key, + kind string not null, + who string not null, + what string not null, + amount integer, + + unique (who, what, kind) + )`) + if err != nil { + log.Fatal().Msgf("could not create goals db: %s", err) + } +} + +var registerSelf = regexp.MustCompile(`(?i)^register (?Pcompetition|goal) (?P[[:punct:][:alnum:]]+)\s?(?P[[:digit:]]+)?`) +var registerOther = regexp.MustCompile(`(?i)^register (?Pcompetition|goal) for (?P[[:punct:][:alnum:]]+) (?P[[:punct:][:alnum:]]+)\s?(?P[[:digit:]]+)?`) +var deRegisterSelf = regexp.MustCompile(`(?i)^deregister (?Pcompetition|goal) (?P[[:punct:][:alnum:]]+)`) +var deRegisterOther = regexp.MustCompile(`(?i)^deregister (?Pcompetition|goal) for (?P[[:punct:][:alnum:]]+) (?P.*)`) +var checkSelf = regexp.MustCompile(`(?i)^check (?Pcompetition|goal) (?P[[:punct:][:alnum:]]+)`) +var checkOther = regexp.MustCompile(`(?i)^check (?Pcompetition|goal) for (?P[[:punct:][:alnum:]]+) (?P[[:punct:][:alnum:]]+)`) + +func (p *GoalsPlugin) message(conn bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { + body := strings.TrimSpace(message.Body) + who := message.User.Name + ch := message.Channel + + if registerSelf.MatchString(body) { + c := parseCmd(registerSelf, body) + amount, _ := strconv.Atoi(c["amount"]) + return p.register(conn, ch, c["type"], c["what"], who, amount) + } + if registerOther.MatchString(body) { + c := parseCmd(registerSelf, body) + amount, _ := strconv.Atoi(c["amount"]) + return p.register(conn, ch, c["type"], c["what"], c["who"], amount) + } + if deRegisterSelf.MatchString(body) { + c := parseCmd(deRegisterSelf, body) + return p.deregister(conn, ch, c["type"], c["what"], who) + } + if deRegisterOther.MatchString(body) { + c := parseCmd(deRegisterOther, body) + return p.deregister(conn, ch, c["type"], c["what"], c["who"]) + } + if checkSelf.MatchString(body) { + c := parseCmd(checkSelf, body) + return p.check(conn, ch, c["type"], c["what"], who) + } + if checkOther.MatchString(body) { + c := parseCmd(checkOther, body) + return p.check(conn, ch, c["type"], c["what"], c["who"]) + } + return false +} + +func (p *GoalsPlugin) register(c bot.Connector, ch, kind, what, who string, howMuch int) bool { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("registering %s %s for %s in amount %d", kind, what, who, howMuch)) + 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)) + return true + } + 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.") + return true + } + p.b.Send(c, bot.Message, ch, fmt.Sprintf("%s created", kind)) + return true +} + +func (p *GoalsPlugin) deregister(c bot.Connector, ch, kind, what, who string) bool { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("deregistering %s for %s", what, who)) + g, err := p.getGoal(kind, who, what) + 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.") + return true + } + 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.") + return true + } + p.b.Send(c, bot.Message, ch, fmt.Sprintf("%s %s deregistered", kind, what)) + return true +} + +func (p *GoalsPlugin) check(c bot.Connector, ch, kind, what, who string) bool { + return p.checkGoal(c, ch, what, who) +} + +func (p *GoalsPlugin) checkCompetition(c bot.Connector, ch, what, who string) bool { + return true +} + +func (p *GoalsPlugin) checkGoal(c bot.Connector, ch, what, who string) bool { + kind := "goal" + p.b.Send(c, bot.Message, ch, fmt.Sprintf("checking %s for %s", what, who)) + g, err := p.getGoal(kind, who, what) + if err != nil { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find %s %s", kind, what)) + } + + item, err := counter.GetItem(p.db, who, what) + if err != nil { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) + return true + } + + perc := float64(item.Count) / float64(g.Amount) * 100.0 + + m := fmt.Sprintf("You have %d out of %d for %s. You're %.2f%% of the way there!", + item.Count, g.Amount, what, perc) + p.b.Send(c, bot.Message, ch, m) + + return true +} + +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." + msg += fmt.Sprintf("\nRegister with `%s` for yourself", registerSelf) + msg += fmt.Sprintf("\nRegister with `%s` for other people", registerOther) + msg += fmt.Sprintf("\nDeregister with `%s` for yourself", deRegisterSelf) + msg += fmt.Sprintf("\nDeregister with `%s` for other people", deRegisterOther) + msg += fmt.Sprintf("\nCheck with `%s` for yourself", checkSelf) + msg += fmt.Sprintf("\nCheck with `%s` for other people", checkOther) + 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 { + ID int64 + 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, + } +} + +func (p *GoalsPlugin) getGoal(kind, who, what string) (*goal, error) { + g := &goal{gp: p} + err := p.db.Get(g, `select * from goals where kind = ? and who = ? and what = ?`, + kind, who, what) + if err != nil { + return nil, err + } + return g, nil +} + +func (g *goal) Save() error { + res, err := g.gp.db.Exec(`insert or replace into goals (who, what, kind, amount) values (?, ?, ? ,?)`, + g.Who, g.What, g.Kind, g.Amount) + if err != nil { + return err + } + dbID, err := res.LastInsertId() + if err == nil && dbID != g.ID && g.ID == 0 { + g.ID = dbID + } + return nil +} + +func (g goal) Delete() error { + if g.ID == -1 { + return nil + } + _, err := g.gp.db.Exec(`delete from goals where id = ?`, g.ID) + return err +} + +func (p *GoalsPlugin) update(u counter.Update) { + log.Debug().Msgf("entered update for %#v", u) + _, err := p.getGoal("goal", u.Who, u.What) + if err != nil { + log.Error().Err(err).Msgf("could not get goal for %#v", u) + return + } + chs := p.cfg.GetArray("channels", []string{}) + c := p.b.DefaultConnector() + for _, ch := range chs { + p.checkGoal(c, ch, u.What, u.Who) + } +}