From 0bb6a7e74753d71d25b7319a9feb51267132c494 Mon Sep 17 00:00:00 2001
From: Chris Sexton <chrissexton@users.noreply.github.com>
Date: Tue, 26 May 2020 11:38:55 -0400
Subject: [PATCH] goals: mostly done, time to try it

---
 main.go                              |   2 +-
 plugins/beers/beers.go               |   8 +-
 plugins/beers/beers_test.go          |  21 ++--
 plugins/counter/counter.go           |  46 ++++++--
 plugins/counter/counter_test.go      |  24 ++--
 plugins/{counter => }/goals/goals.go | 158 ++++++++++++++++++---------
 6 files changed, 175 insertions(+), 84 deletions(-)
 rename plugins/{counter => }/goals/goals.go (65%)

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)
+			}
+		}
 	}
 }