diff --git a/main.go b/main.go index 7e75437..44e4508 100644 --- a/main.go +++ b/main.go @@ -11,7 +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/goals" "github.com/velour/catbase/plugins/meme" "github.com/velour/catbase/plugins/sms" "github.com/velour/catbase/plugins/twitter" diff --git a/plugins/beers/beers.go b/plugins/beers/beers.go index ffdadb9..f221659 100644 --- a/plugins/beers/beers.go +++ b/plugins/beers/beers.go @@ -75,6 +75,12 @@ func New(b bot.Bot) *BeersPlugin { p.registerWeb() + token := p.c.Get("Untappd.Token", "NONE") + if token == "NONE" || token == "" { + log.Error().Msgf("No untappd token. Checking disabled.") + return p + } + for _, channel := range p.c.GetArray("Untappd.Channels", []string{}) { go p.untappdLoop(b.DefaultConnector(), channel) } @@ -229,7 +235,7 @@ func (p *BeersPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, } func getUserBeers(db *sqlx.DB, user string) counter.Item { - booze, _ := counter.GetItem(db, user, itemName) + booze, _ := counter.GetUserItem(db, user, itemName) return booze } diff --git a/plugins/beers/beers_test.go b/plugins/beers/beers_test.go index d2c3ba2..851cd9e 100644 --- a/plugins/beers/beers_test.go +++ b/plugins/beers/beers_test.go @@ -3,10 +3,11 @@ package beers import ( - "github.com/velour/catbase/plugins/cli" "strings" "testing" + "github.com/velour/catbase/plugins/cli" + "github.com/stretchr/testify/assert" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" @@ -40,7 +41,7 @@ func makeBeersPlugin(t *testing.T) (*BeersPlugin, *bot.MockBot) { func TestCounter(t *testing.T) { _, mb := makeBeersPlugin(t) - i, err := counter.GetItem(mb.DB(), "tester", "test") + i, err := counter.GetUserItem(mb.DB(), "tester", "test") if !assert.Nil(t, err) { t.Log(err) t.Fatal() @@ -55,7 +56,7 @@ func TestImbibe(t *testing.T) { assert.Len(t, mb.Messages, 1) b.message(makeMessage("!imbibe")) assert.Len(t, mb.Messages, 2) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 2, it.Count) } @@ -63,7 +64,7 @@ func TestEq(t *testing.T) { b, mb := makeBeersPlugin(t) b.message(makeMessage("!beers = 3")) assert.Len(t, mb.Messages, 1) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 3, it.Count) } @@ -72,7 +73,7 @@ func TestEqNeg(t *testing.T) { b, mb := makeBeersPlugin(t) b.message(makeMessage("!beers = -3")) assert.Len(t, mb.Messages, 1) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 0, it.Count) } @@ -83,7 +84,7 @@ func TestEqZero(t *testing.T) { 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) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 0, it.Count) } @@ -94,7 +95,7 @@ func TestBeersPlusEq(t *testing.T) { assert.Len(t, mb.Messages, 1) b.message(makeMessage("beers += 5")) assert.Len(t, mb.Messages, 2) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 10, it.Count) } @@ -102,11 +103,11 @@ func TestBeersPlusEq(t *testing.T) { func TestPuke(t *testing.T) { b, mb := makeBeersPlugin(t) b.message(makeMessage("beers += 5")) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 5, it.Count) b.message(makeMessage("puke")) - it, err = counter.GetItem(mb.DB(), "tester", itemName) + it, err = counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 0, it.Count) } @@ -114,7 +115,7 @@ func TestPuke(t *testing.T) { func TestBeersReport(t *testing.T) { b, mb := makeBeersPlugin(t) b.message(makeMessage("beers += 5")) - it, err := counter.GetItem(mb.DB(), "tester", itemName) + it, err := counter.GetUserItem(mb.DB(), "tester", itemName) assert.Nil(t, err) assert.Equal(t, 5, it.Count) b.message(makeMessage("beers")) diff --git a/plugins/counter/counter.go b/plugins/counter/counter.go index 7f80b18..0b30ea7 100644 --- a/plugins/counter/counter.go +++ b/plugins/counter/counter.go @@ -139,8 +139,33 @@ func MkAlias(db *sqlx.DB, aliasName, pointsTo string) (*alias, error) { return &alias{db, id, aliasName, pointsTo}, nil } -// GetItem returns a specific counter for a subject -func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) { +// GetUserItem returns a specific counter for all subjects +func GetItem(db *sqlx.DB, itemName string) ([]Item, error) { + itemName = trimUnicode(itemName) + var items []Item + var a alias + if err := db.Get(&a, `select * from counter_alias where item=?`, itemName); err == nil { + itemName = a.PointsTo + } else { + log.Error().Err(err).Interface("alias", a) + } + + err := db.Select(&items, `select * from counter where item= ?`, itemName) + if err != nil { + return nil, err + } + log.Debug(). + Str("itemName", itemName). + Interface("items", items). + Msg("got item") + for _, i := range items { + i.DB = db + } + return items, nil +} + +// GetUserItem returns a specific counter for a subject +func GetUserItem(db *sqlx.DB, nick, itemName string) (Item, error) { itemName = trimUnicode(itemName) var item Item item.DB = db @@ -170,6 +195,7 @@ func GetItem(db *sqlx.DB, nick, itemName string) (Item, error) { return item, nil } +// GetUserItem returns a specific counter for a subject // Create saves a counter func (i *Item) Create() error { res, err := i.Exec(`insert into counter (nick, item, count) values (?, ?, ?);`, @@ -376,7 +402,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess subject := strings.ToLower(nick) itemName := strings.ToLower(parts[1]) - it, err := GetItem(p.DB, subject, itemName) + it, err := GetUserItem(p.DB, subject, itemName) if err != nil { log.Error(). Err(err). @@ -417,7 +443,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess } var item Item - item, err := GetItem(p.DB, subject, itemName) + item, err := GetUserItem(p.DB, subject, itemName) switch { case err == sql.ErrNoRows: p.Bot.Send(c, bot.Message, channel, fmt.Sprintf("I don't think %s has any %s.", @@ -455,7 +481,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess if strings.HasSuffix(parts[0], "++") { // ++ those fuckers - item, err := GetItem(p.DB, subject, itemName) + item, err := GetUserItem(p.DB, subject, itemName) if err != nil { log.Error(). Err(err). @@ -472,7 +498,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess return true } else if strings.HasSuffix(parts[0], "--") { // -- those fuckers - item, err := GetItem(p.DB, subject, itemName) + item, err := GetUserItem(p.DB, subject, itemName) if err != nil { log.Error(). Err(err). @@ -503,7 +529,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess if parts[1] == "+=" { // += those fuckers - item, err := GetItem(p.DB, subject, itemName) + item, err := GetUserItem(p.DB, subject, itemName) if err != nil { log.Error(). Err(err). @@ -521,7 +547,7 @@ func (p *CounterPlugin) message(c bot.Connector, kind bot.Kind, message msg.Mess return true } else if parts[1] == "-=" { // -= those fuckers - item, err := GetItem(p.DB, subject, itemName) + item, err := GetUserItem(p.DB, subject, itemName) if err != nil { log.Error(). Err(err). @@ -565,7 +591,7 @@ func (p *CounterPlugin) checkMatch(c bot.Connector, message msg.Message) bool { itemName := strings.ToLower(submatches[1]) // We will specifically allow :tea: to keep compatability - item, err := GetItem(p.DB, nick, itemName) + item, err := GetUserItem(p.DB, nick, itemName) if err != nil || (item.Count == 0 && item.Item != ":tea:") { log.Error(). Err(err). @@ -630,7 +656,7 @@ func (p *CounterPlugin) handleCounterAPI(w http.ResponseWriter, r *http.Request) w.Write(j) return } - item, err := GetItem(p.DB, info.User, info.Thing) + item, err := GetUserItem(p.DB, info.User, info.Thing) if err != nil { log.Error(). Err(err). diff --git a/plugins/counter/counter_test.go b/plugins/counter/counter_test.go index f108b96..4a57b5b 100644 --- a/plugins/counter/counter_test.go +++ b/plugins/counter/counter_test.go @@ -43,7 +43,7 @@ func TestMkAlias(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage("mkalias fuck mornings")) c.message(makeMessage("fuck++")) - item, err := GetItem(mb.DB(), "tester", "mornings") + item, err := GetUserItem(mb.DB(), "tester", "mornings") assert.Nil(t, err) assert.Equal(t, 1, item.Count) } @@ -54,7 +54,7 @@ func TestRmAlias(t *testing.T) { c.message(makeMessage("mkalias fuck mornings")) c.message(makeMessage("rmalias fuck")) c.message(makeMessage("fuck++")) - item, err := GetItem(mb.DB(), "tester", "mornings") + item, err := GetUserItem(mb.DB(), "tester", "mornings") assert.Nil(t, err) assert.Equal(t, 0, item.Count) } @@ -64,7 +64,7 @@ func TestThreeSentencesExists(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage(":beer:++")) c.message(makeMessage(":beer:. Earl Grey. Hot.")) - item, err := GetItem(mb.DB(), "tester", ":beer:") + item, err := GetUserItem(mb.DB(), "tester", ":beer:") assert.Nil(t, err) assert.Equal(t, 2, item.Count) } @@ -72,9 +72,9 @@ func TestThreeSentencesExists(t *testing.T) { func TestThreeSentencesNotExists(t *testing.T) { mb, c := setup(t) assert.NotNil(t, c) - item, err := GetItem(mb.DB(), "tester", ":beer:") + item, err := GetUserItem(mb.DB(), "tester", ":beer:") c.message(makeMessage(":beer:. Earl Grey. Hot.")) - item, err = GetItem(mb.DB(), "tester", ":beer:") + item, err = GetUserItem(mb.DB(), "tester", ":beer:") assert.Nil(t, err) assert.Equal(t, 0, item.Count) } @@ -84,7 +84,7 @@ func TestTeaEarlGreyHot(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage("Tea. Earl Grey. Hot.")) c.message(makeMessage("Tea. Earl Grey. Hot.")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 2, item.Count) } @@ -94,7 +94,7 @@ func TestTeaTwoPeriods(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage("Tea. Earl Grey.")) c.message(makeMessage("Tea. Earl Grey.")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 0, item.Count) } @@ -104,7 +104,7 @@ func TestTeaMultiplePeriods(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage("Tea. Earl Grey. Spiked. Hot.")) c.message(makeMessage("Tea. Earl Grey. Spiked. Hot.")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 2, item.Count) } @@ -115,7 +115,7 @@ func TestTeaGreenHot(t *testing.T) { 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:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 3, item.Count) } @@ -125,7 +125,7 @@ func TestTeaUnrelated(t *testing.T) { assert.NotNil(t, c) c.message(makeMessage("Tea.")) c.message(makeMessage("Tea. It's great.")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 0, item.Count) } @@ -134,7 +134,7 @@ 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")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 0, item.Count) } @@ -142,7 +142,7 @@ func TestTeaUnicodeJapanese(t *testing.T) { mb, c := setup(t) assert.NotNil(t, c) c.message(makeMessage("Tea. ใŠใกใ‚„. Hot.")) - item, err := GetItem(mb.DB(), "tester", ":tea:") + item, err := GetUserItem(mb.DB(), "tester", ":tea:") assert.Nil(t, err) assert.Equal(t, 1, item.Count) } diff --git a/plugins/counter/goals/goals.go b/plugins/goals/goals.go similarity index 65% rename from plugins/counter/goals/goals.go rename to plugins/goals/goals.go index 7c23a32..263c6f2 100644 --- a/plugins/counter/goals/goals.go +++ b/plugins/goals/goals.go @@ -3,6 +3,7 @@ package goals import ( "fmt" "regexp" + "sort" "strconv" "strings" @@ -64,97 +65,135 @@ func (p *GoalsPlugin) message(conn bot.Connector, kind bot.Kind, message msg.Mes 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) { + p.register(conn, ch, c["type"], c["what"], who, amount) + } else 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) { + p.register(conn, ch, c["type"], c["what"], c["who"], amount) + } else if deRegisterSelf.MatchString(body) { c := parseCmd(deRegisterSelf, body) - return p.deregister(conn, ch, c["type"], c["what"], who) - } - if deRegisterOther.MatchString(body) { + p.deregister(conn, ch, c["type"], c["what"], who) + } else if deRegisterOther.MatchString(body) { c := parseCmd(deRegisterOther, body) - return p.deregister(conn, ch, c["type"], c["what"], c["who"]) - } - if checkSelf.MatchString(body) { + p.deregister(conn, ch, c["type"], c["what"], c["who"]) + } else if checkSelf.MatchString(body) { c := parseCmd(checkSelf, body) - return p.check(conn, ch, c["type"], c["what"], who) - } - if checkOther.MatchString(body) { + p.check(conn, ch, c["type"], c["what"], who) + } else if checkOther.MatchString(body) { c := parseCmd(checkOther, body) - return p.check(conn, ch, c["type"], c["what"], c["who"]) + p.check(conn, ch, c["type"], c["what"], c["who"]) + } else { + return false } - return false + return true } -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)) +func (p *GoalsPlugin) register(c bot.Connector, ch, kind, what, who string, howMuch int) { 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 + return } 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 + return } 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) +func (p *GoalsPlugin) deregister(c bot.Connector, ch, kind, what, who string) { + g, err := p.getGoalKind(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 + return } 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 + return } 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) check(c bot.Connector, ch, kind, what, who string) { + if kind == "goal" { + p.checkGoal(c, ch, what, who) + return + } + p.checkCompetition(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)) +func (p *GoalsPlugin) checkCompetition(c bot.Connector, ch, what, who string) { + items, err := counter.GetItem(p.db, what) + if err != nil || len(items) == 0 { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find any %s", what)) + return } - item, err := counter.GetItem(p.db, who, what) + 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)) +} + +func (p *GoalsPlugin) checkGoal(c bot.Connector, ch, what, who string) { + g, err := p.getGoalKind("goal", who, what) + if err != nil { + p.b.Send(c, bot.Message, ch, fmt.Sprintf("I couldn't find %s", what)) + } + + item, err := counter.GetUserItem(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 + return } 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) + 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 { + 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 + return } func (p *GoalsPlugin) help(c bot.Connector, kind bot.Kind, message msg.Message, args ...interface{}) bool { @@ -205,7 +244,20 @@ func (p *GoalsPlugin) newGoal(kind, who, what string, amount int) goal { } } -func (p *GoalsPlugin) getGoal(kind, who, what string) (*goal, error) { +func (p *GoalsPlugin) getGoal(who, what string) ([]*goal, error) { + gs := []*goal{} + err := p.db.Select(&gs, `select * from goals where who = ? and what = ?`, + who, what) + 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) { g := &goal{gp: p} err := p.db.Get(g, `select * from goals where kind = ? and who = ? and what = ?`, kind, who, what) @@ -238,14 +290,20 @@ func (g goal) Delete() error { func (p *GoalsPlugin) update(u counter.Update) { log.Debug().Msgf("entered update for %#v", u) - _, err := p.getGoal("goal", u.Who, u.What) + gs, err := p.getGoal(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) + for _, g := range gs { + for _, ch := range chs { + if g.Kind == "goal" { + p.checkGoal(c, ch, u.What, u.Who) + } else { + p.checkCompetition(c, ch, u.What, u.Who) + } + } } }