From cdff69abdc27e1996c3c3cab50e95cb268ced7ae Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Mon, 15 Jul 2019 12:46:24 -0400 Subject: [PATCH 1/5] bid: Add a skeleton of an n-gate bidding game * webshit module will house any logic needed for the game * newsbid is the top-level plugin to be used for the bot interface --- go.mod | 5 +- go.sum | 6 +- plugins/newsbid/newsbid.go | 25 ++++++++ plugins/newsbid/webshit/webshit.go | 76 +++++++++++++++++++++++++ plugins/newsbid/webshit/webshit_test.go | 30 ++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 plugins/newsbid/newsbid.go create mode 100644 plugins/newsbid/webshit/webshit.go create mode 100644 plugins/newsbid/webshit/webshit_test.go diff --git a/go.mod b/go.mod index 6d3988c..fa4a86a 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,8 @@ module github.com/velour/catbase require ( - github.com/PuerkitoBio/goquery v1.5.0 // indirect + github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c + github.com/PuerkitoBio/goquery v1.5.0 github.com/armon/go-radix v1.0.0 // indirect github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff github.com/go-sql-driver/mysql v1.4.1 // indirect @@ -26,7 +27,7 @@ require ( github.com/velour/chat v0.0.0-20180713122344-fd1d1606cb89 github.com/velour/velour v0.0.0-20160303155839-8e090e68d158 golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 // indirect - golang.org/x/net v0.0.0-20190326090315-15845e8f865b // indirect + golang.org/x/net v0.0.0-20190606173856-1492cefac77f // indirect gonum.org/v1/gonum v0.0.0-20190325211145-e42c1265cdd5 // indirect google.golang.org/appengine v1.4.0 // indirect gopkg.in/sourcemap.v1 v1.0.5 // indirect diff --git a/go.sum b/go.sum index a5f9830..646cbc7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE= github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c h1:bJ0HbTMaInVjakxM76G+2gsmbKTdHzpTUGyLGYxdMO0= +github.com/PaulRosset/go-hacknews v0.0.0-20170815075127-4aad99273a3c/go.mod h1:8+24kIp7vJsYy0GmQDDNnPwAYEWkl3OcaPxJSDAfe1U= github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= @@ -74,8 +76,8 @@ golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190326090315-15845e8f865b h1:LlDMQZ0I/u8J45sbt31TecpsFNErRGwDgS4WvT9hKzE= -golang.org/x/net v0.0.0-20190326090315-15845e8f865b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0= +golang.org/x/net v0.0.0-20190606173856-1492cefac77f/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= diff --git a/plugins/newsbid/newsbid.go b/plugins/newsbid/newsbid.go new file mode 100644 index 0000000..c2355a1 --- /dev/null +++ b/plugins/newsbid/newsbid.go @@ -0,0 +1,25 @@ +package newsbid + +import ( + "github.com/jmoiron/sqlx" + "github.com/velour/catbase/bot" + "github.com/velour/catbase/bot/msg" +) + +type NewsBid struct { + bot bot.Bot + db *sqlx.DB +} + +func New(b bot.Bot) *NewsBid { + p := &NewsBid{ + bot: b, + db: b.DB(), + } + p.bot.Register(p, bot.Message, p.message) + return p +} + +func (p *NewsBid) message(conn bot.Connector, k bot.Kind, message msg.Message, args ...interface{}) bool { + return false +} diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go new file mode 100644 index 0000000..a06b6eb --- /dev/null +++ b/plugins/newsbid/webshit/webshit.go @@ -0,0 +1,76 @@ +package webshit + +import ( + "bytes" + "fmt" + "github.com/PaulRosset/go-hacknews" + "github.com/PuerkitoBio/goquery" + "github.com/jmoiron/sqlx" + "github.com/mmcdole/gofeed" + "net/url" +) + +type Webshit struct { + db *sqlx.DB +} + +func New(db *sqlx.DB) *Webshit { + w := &Webshit{db} + w.setup() + return w +} + +// setup will create any necessary SQL tables and populate them with minimal data +func (w *Webshit) setup() { +} + +// GetHeadlines will return the current possible news headlines for bidding +func (w *Webshit) GetHeadlines() ([]hacknews.Post, error) { + news := hacknews.Initializer{Story: "topstories", NbPosts: 10} + ids, err := news.GetCodesStory() + if err != nil { + return nil, err + } + posts, err := news.GetPostStory(ids) + if err != nil { + return nil, err + } + return posts, nil +} + +type Weekly []string + +// GetWeekly will return the headlines in the last webshit weekly report +func (w *Webshit) GetWeekly() (Weekly, error) { + fp := gofeed.NewParser() + feed, err := fp.ParseURL("http://n-gate.com/hackernews/index.rss") + if err != nil { + return nil, err + } + if len(feed.Items) <= 0 { + return nil, fmt.Errorf("no webshit weekly found") + } + + buf := bytes.NewBufferString(feed.Items[0].Description) + doc, err := goquery.NewDocumentFromReader(buf) + if err != nil { + return nil, err + } + + var items []string + doc.Find(".storylink").Each(func(i int, s *goquery.Selection) { + items = append(items, s.Find("a").Text()) + }) + + return items, nil +} + +// GetBalances returns the current balance for all known users +// Any unknown user has a default balance on their first bid +func (w *Webshit) GetBalances() { +} + +// Bid allows a user to place a bid on a particular story +func (w *Webshit) Bid(user string, amount int, URL url.URL) error { + return nil +} diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go new file mode 100644 index 0000000..4d0c278 --- /dev/null +++ b/plugins/newsbid/webshit/webshit_test.go @@ -0,0 +1,30 @@ +package webshit + +import ( + "github.com/jmoiron/sqlx" + "testing" + + _ "github.com/mattn/go-sqlite3" +) + +func make(t *testing.T) *Webshit { + db := sqlx.MustOpen("sqlite3", "file::memory:?mode=memory&cache=shared") + w := New(db) + if w.db != db { + t.Fail() + } + return w +} + +func TestWebshit_GetWeekly(t *testing.T) { + w := make(t) + weekly, err := w.GetWeekly() + if err != nil { + t.Errorf("Could not get weekly: %s", err) + t.Fail() + } + if len(weekly) < 5 { + t.Errorf("Weekly content:\n%+v", weekly) + t.Fail() + } +} From 04239ec807f00ba14617c4d723e264f1c18dd6cc Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Mon, 15 Jul 2019 13:39:40 -0400 Subject: [PATCH 2/5] bid: add some tests and tables --- plugins/newsbid/webshit/webshit.go | 106 ++++++++++++++++++++++-- plugins/newsbid/webshit/webshit_test.go | 43 +++++++--- 2 files changed, 131 insertions(+), 18 deletions(-) diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go index a06b6eb..67ea1c9 100644 --- a/plugins/newsbid/webshit/webshit.go +++ b/plugins/newsbid/webshit/webshit.go @@ -7,13 +7,31 @@ import ( "github.com/PuerkitoBio/goquery" "github.com/jmoiron/sqlx" "github.com/mmcdole/gofeed" + "github.com/rs/zerolog/log" + "net/http" "net/url" + "strings" ) type Webshit struct { db *sqlx.DB } +type Weekly []string + +type Story struct { + Title string + URL string +} + +type Bid struct { + ID int + User string + Title string + URL string + Bid int +} + func New(db *sqlx.DB) *Webshit { w := &Webshit{db} w.setup() @@ -22,10 +40,26 @@ func New(db *sqlx.DB) *Webshit { // setup will create any necessary SQL tables and populate them with minimal data func (w *Webshit) setup() { + if _, err := w.db.Exec(`create table if not exists webshit_bids ( + id integer primary key, + user string, + title string, + url string, + bid integer + )`); err != nil { + log.Fatal().Err(err) + } + if _, err := w.db.Exec(`create table if not exists webshit_balances ( + user string primary key, + balance int, + score int + )`); err != nil { + log.Fatal().Err(err) + } } // GetHeadlines will return the current possible news headlines for bidding -func (w *Webshit) GetHeadlines() ([]hacknews.Post, error) { +func (w *Webshit) GetHeadlines() ([]Story, error) { news := hacknews.Initializer{Story: "topstories", NbPosts: 10} ids, err := news.GetCodesStory() if err != nil { @@ -35,11 +69,16 @@ func (w *Webshit) GetHeadlines() ([]hacknews.Post, error) { if err != nil { return nil, err } - return posts, nil + var stories []Story + for _, p := range posts { + stories = append(stories, Story{ + Title: p.Title, + URL: p.Url, + }) + } + return stories, nil } -type Weekly []string - // GetWeekly will return the headlines in the last webshit weekly report func (w *Webshit) GetWeekly() (Weekly, error) { fp := gofeed.NewParser() @@ -67,10 +106,63 @@ func (w *Webshit) GetWeekly() (Weekly, error) { // GetBalances returns the current balance for all known users // Any unknown user has a default balance on their first bid -func (w *Webshit) GetBalances() { +func (w *Webshit) GetBalance(user string) int { + q := `select balance from webshit_balances where user=?` + var balance int + err := w.db.Get(&balance, q, user) + if err != nil { + return 100 + } + return balance } // Bid allows a user to place a bid on a particular story -func (w *Webshit) Bid(user string, amount int, URL url.URL) error { - return nil +func (w *Webshit) Bid(user string, amount int, URL string) error { + bal := w.GetBalance(user) + if bal < amount { + return fmt.Errorf("cannot bid more than balance, %d", bal) + } + story, err := w.getStoryByURL(URL) + if err != nil { + return err + } + + // Need a transaction here to deduct from the users balance (or create it) + _, err = w.db.Exec(`insert into webshit_bids (user,title,url,bid) values (?,?,?,?)`, + user, story.Title, story.URL, amount) + + return err +} + +// getStoryByURL scrapes the URL for a title +func (w *Webshit) getStoryByURL(URL string) (Story, error) { + u, err := url.Parse(URL) + if err != nil { + return Story{}, err + } + if u.Host != "news.ycombinator.com" { + return Story{}, fmt.Errorf("expected HN link") + } + res, err := http.Get(URL) + if err != nil { + return Story{}, err + } + defer res.Body.Close() + if res.StatusCode != 200 { + return Story{}, fmt.Errorf("bad response code: %d", res.StatusCode) + } + + // Load the HTML document + doc, err := goquery.NewDocumentFromReader(res.Body) + if err != nil { + return Story{}, err + } + + // Find the review items + title := doc.Find("title").Text() + title = strings.ReplaceAll(title, " | Hacker News", "") + return Story{ + Title: title, + URL: URL, + }, nil } diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go index 4d0c278..bae1ae9 100644 --- a/plugins/newsbid/webshit/webshit_test.go +++ b/plugins/newsbid/webshit/webshit_test.go @@ -2,6 +2,7 @@ package webshit import ( "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" "testing" _ "github.com/mattn/go-sqlite3" @@ -10,21 +11,41 @@ import ( func make(t *testing.T) *Webshit { db := sqlx.MustOpen("sqlite3", "file::memory:?mode=memory&cache=shared") w := New(db) - if w.db != db { - t.Fail() - } + assert.Equal(t, w.db, db) return w } func TestWebshit_GetWeekly(t *testing.T) { w := make(t) weekly, err := w.GetWeekly() - if err != nil { - t.Errorf("Could not get weekly: %s", err) - t.Fail() - } - if len(weekly) < 5 { - t.Errorf("Weekly content:\n%+v", weekly) - t.Fail() - } + assert.Nil(t, err) + assert.NotEmpty(t, weekly) +} + +func TestWebshit_GetHeadlines(t *testing.T) { + w := make(t) + headlines, err := w.GetHeadlines() + assert.Nil(t, err) + assert.NotEmpty(t, headlines) +} + +func TestWebshit_getStoryByURL(t *testing.T) { + w := make(t) + expected := "Developer Tropes: “Google Does It”" + s, err := w.getStoryByURL("https://news.ycombinator.com/item?id=20432887") + assert.Nil(t, err) + assert.Equal(t, s.Title, expected) +} + +func TestWebshit_getStoryByURL_BadURL(t *testing.T) { + w := make(t) + _, err := w.getStoryByURL("https://google.com") + assert.Error(t, err) +} + +func TestWebshit_GetBalance(t *testing.T) { + w := make(t) + expected := 100 + actual := w.GetBalance("foo") + assert.Equal(t, expected, actual) } From dd0f9efeaedcc3d8a062dda91b3cbd94252e37de Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Mon, 15 Jul 2019 14:57:23 -0400 Subject: [PATCH 3/5] bid: add check routine --- plugins/newsbid/webshit/webshit.go | 131 +++++++++++++++++++++--- plugins/newsbid/webshit/webshit_test.go | 2 +- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go index 67ea1c9..a558f3c 100644 --- a/plugins/newsbid/webshit/webshit.go +++ b/plugins/newsbid/webshit/webshit.go @@ -11,14 +11,13 @@ import ( "net/http" "net/url" "strings" + "time" ) type Webshit struct { db *sqlx.DB } -type Weekly []string - type Story struct { Title string URL string @@ -32,6 +31,12 @@ type Bid struct { Bid int } +type WeeklyResult struct { + User string + Won int + Lost int +} + func New(db *sqlx.DB) *Webshit { w := &Webshit{db} w.setup() @@ -45,7 +50,8 @@ func (w *Webshit) setup() { user string, title string, url string, - bid integer + bid integer, + created integer )`); err != nil { log.Fatal().Err(err) } @@ -58,6 +64,75 @@ func (w *Webshit) setup() { } } +func (w *Webshit) Check() (map[string]WeeklyResult, error) { + stories, published, err := w.GetWeekly() + if err != nil { + return nil, err + } + + var bids []Bid + if err = w.db.Get(&bids, `select * from webshit_bids where created < ?`, + published.Unix()); err != nil { + return nil, err + } + + // Assuming no bids earlier than the weekly means there hasn't been a new weekly + if len(bids) == 0 { + return nil, nil + } + + storyMap := map[string]Story{} + for _, s := range stories { + storyMap[s.Title] = s + } + + wr := w.checkBids(bids, storyMap) + + // Update all balance scores in a tx + if err := w.updateScores(wr); err != nil { + return nil, err + } + + // Delete all those bids + if _, err = w.db.Exec(`delete from webshit_bids where created < ?`, + published.Unix()); err != nil { + return nil, err + } + + // Set all balances to 100 + if _, err = w.db.Exec(`update webshit_balances set balance=100`); err != nil { + return nil, err + } + + return wr, nil +} + +func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) map[string]WeeklyResult { + wr := map[string]WeeklyResult{} + for _, b := range bids { + win, loss := 0, 0 + if s, ok := storyMap[b.Title]; ok { + log.Info().Interface("story", s).Msg("won bid") + win = b.Bid + } else { + log.Info().Interface("story", s).Msg("lost bid") + loss = b.Bid + } + if res, ok := wr[b.User]; !ok { + wr[b.User] = WeeklyResult{ + User: b.User, + Won: win, + Lost: loss, + } + } else { + res.Won = win + res.Lost = loss + wr[b.User] = res + } + } + return wr +} + // GetHeadlines will return the current possible news headlines for bidding func (w *Webshit) GetHeadlines() ([]Story, error) { news := hacknews.Initializer{Story: "topstories", NbPosts: 10} @@ -80,28 +155,34 @@ func (w *Webshit) GetHeadlines() ([]Story, error) { } // GetWeekly will return the headlines in the last webshit weekly report -func (w *Webshit) GetWeekly() (Weekly, error) { +func (w *Webshit) GetWeekly() ([]Story, *time.Time, error) { fp := gofeed.NewParser() feed, err := fp.ParseURL("http://n-gate.com/hackernews/index.rss") if err != nil { - return nil, err + return nil, nil, err } if len(feed.Items) <= 0 { - return nil, fmt.Errorf("no webshit weekly found") + return nil, nil, fmt.Errorf("no webshit weekly found") } + published := feed.PublishedParsed + buf := bytes.NewBufferString(feed.Items[0].Description) doc, err := goquery.NewDocumentFromReader(buf) if err != nil { - return nil, err + return nil, nil, err } - var items []string + var items []Story doc.Find(".storylink").Each(func(i int, s *goquery.Selection) { - items = append(items, s.Find("a").Text()) + story := Story{ + Title: s.Find("a").Text(), + URL: s.Find("a").AttrOr("src", ""), + } + items = append(items, story) }) - return items, nil + return items, published, nil } // GetBalances returns the current balance for all known users @@ -127,9 +208,20 @@ func (w *Webshit) Bid(user string, amount int, URL string) error { return err } - // Need a transaction here to deduct from the users balance (or create it) - _, err = w.db.Exec(`insert into webshit_bids (user,title,url,bid) values (?,?,?,?)`, - user, story.Title, story.URL, amount) + tx := w.db.MustBegin() + _, err = tx.Exec(`insert into webshit_bids (user,title,url,bid,created) values (?,?,?,?,?)`, + user, story.Title, story.URL, amount, time.Now().Unix()) + if err != nil { + tx.Rollback() + return err + } + _, err = tx.Exec(`update webshit_balances set balance=? where user=?`, + bal-amount, user) + if err != nil { + tx.Rollback() + return err + } + tx.Commit() return err } @@ -166,3 +258,16 @@ func (w *Webshit) getStoryByURL(URL string) (Story, error) { URL: URL, }, nil } + +func (w *Webshit) updateScores(results map[string]WeeklyResult) error { + tx := w.db.MustBegin() + for _, res := range results { + if _, err := tx.Exec(`update webshit_balances set score=score+? where user=?`, + res.Won-res.Lost, res.User); err != nil { + tx.Rollback() + return err + } + } + err := tx.Commit() + return err +} diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go index bae1ae9..7caddc9 100644 --- a/plugins/newsbid/webshit/webshit_test.go +++ b/plugins/newsbid/webshit/webshit_test.go @@ -17,7 +17,7 @@ func make(t *testing.T) *Webshit { func TestWebshit_GetWeekly(t *testing.T) { w := make(t) - weekly, err := w.GetWeekly() + weekly, _, err := w.GetWeekly() assert.Nil(t, err) assert.NotEmpty(t, weekly) } From 286582417b43065c7a863bfdb18ba311e7ed66c3 Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Mon, 15 Jul 2019 16:55:35 -0400 Subject: [PATCH 4/5] bid: connect scores/bids/etc to the chat --- main.go | 2 + plugins/newsbid/newsbid.go | 84 +++++++++++++++++ plugins/newsbid/webshit/webshit.go | 120 +++++++++++++++++------- plugins/newsbid/webshit/webshit_test.go | 4 +- 4 files changed, 173 insertions(+), 37 deletions(-) diff --git a/main.go b/main.go index 8a1fdae..9e9dcb8 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( "flag" "github.com/velour/catbase/plugins/cli" + "github.com/velour/catbase/plugins/newsbid" "math/rand" "net/http" "os" @@ -124,6 +125,7 @@ func main() { b.AddPlugin(nerdepedia.New(b)) b.AddPlugin(tldr.New(b)) b.AddPlugin(stock.New(b)) + b.AddPlugin(newsbid.New(b)) b.AddPlugin(cli.New(b)) // catches anything left, will always return true b.AddPlugin(fact.New(b)) diff --git a/plugins/newsbid/newsbid.go b/plugins/newsbid/newsbid.go index c2355a1..d26dfd8 100644 --- a/plugins/newsbid/newsbid.go +++ b/plugins/newsbid/newsbid.go @@ -1,25 +1,109 @@ package newsbid import ( + "fmt" "github.com/jmoiron/sqlx" "github.com/velour/catbase/bot" "github.com/velour/catbase/bot/msg" + "github.com/velour/catbase/plugins/newsbid/webshit" + "sort" + "strconv" + "strings" ) type NewsBid struct { bot bot.Bot db *sqlx.DB + ws *webshit.Webshit } func New(b bot.Bot) *NewsBid { + ws := webshit.New(b.DB()) p := &NewsBid{ bot: b, db: b.DB(), + ws: ws, } p.bot.Register(p, bot.Message, p.message) return p } func (p *NewsBid) message(conn bot.Connector, k bot.Kind, message msg.Message, args ...interface{}) bool { + body := strings.ToLower(message.Body) + ch := message.Channel + if message.Command && body == "balance" { + bal := p.ws.GetBalance(message.User.Name) + p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("%s, your current balance is %d.", + message.User.Name, bal)) + return true + } + if message.Command && body == "bids" { + bids, err := p.ws.GetAllBids() + if err != nil { + p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error getting bids: %s", err)) + return true + } + if len(bids) == 0 { + p.bot.Send(conn, bot.Message, ch, "No bids to report.") + return true + } + sort.Slice(bids, func(i, j int) bool { return bids[i].User < bids[j].User }) + out := "Bids:\n" + for _, b := range bids { + out += fmt.Sprintf("%s bid %d on %s\n", b.User, b.Bid, b.Title) + } + p.bot.Send(conn, bot.Message, ch, out) + return true + } + if message.Command && body == "scores" { + bals, err := p.ws.GetAllBalances() + if err != nil { + p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error getting bids: %s", err)) + return true + } + if len(bals) == 0 { + p.bot.Send(conn, bot.Message, ch, "No balances to report.") + return true + } + out := "NGate balances:\n" + for _, b := range bals { + out += fmt.Sprintf("%s has a total score of %d with %d left to bid this session\n", b.User, b.Score, b.Balance) + } + p.bot.Send(conn, bot.Message, ch, out) + return true + + } + if message.Command && strings.HasPrefix(body, "bid") { + parts := strings.Fields(body) + if len(parts) != 3 { + p.bot.Send(conn, bot.Message, ch, "You must bid with an amount and a URL.") + return true + } + amount, _ := strconv.Atoi(parts[1]) + url := parts[2] + if err := p.ws.Bid(message.User.Name, amount, url); err != nil { + p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error placing bid: %s", err)) + } else { + p.bot.Send(conn, bot.Message, ch, "Your bid has been placed.") + } + return true + } + if message.Command && body == "check ngate" { + p.check(conn, ch) + return true + } return false } + +func (p *NewsBid) check(conn bot.Connector, ch string) { + wr, err := p.ws.Check() + if err != nil { + p.bot.Send(conn, bot.Message, ch, fmt.Sprintf("Error checking ngate: %s", err)) + return + } + for _, res := range wr { + msg := fmt.Sprintf("%s: won %d and lost %d for a score of %d", + res.User, res.Won, res.Lost, res.Score) + p.bot.Send(conn, bot.Message, ch, msg) + } +} diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go index a558f3c..2d88bd5 100644 --- a/plugins/newsbid/webshit/webshit.go +++ b/plugins/newsbid/webshit/webshit.go @@ -24,17 +24,29 @@ type Story struct { } type Bid struct { - ID int - User string - Title string - URL string - Bid int + ID int + User string + Title string + URL string + Bid int + Placed int64 +} + +func (b Bid) PlacedParsed() time.Time { + return time.Unix(b.Placed, 0) +} + +type Balance struct { + User string + Balance int + Score int } type WeeklyResult struct { - User string - Won int - Lost int + User string + Won int + Lost int + Score int } func New(db *sqlx.DB) *Webshit { @@ -45,40 +57,36 @@ func New(db *sqlx.DB) *Webshit { // setup will create any necessary SQL tables and populate them with minimal data func (w *Webshit) setup() { - if _, err := w.db.Exec(`create table if not exists webshit_bids ( - id integer primary key, + w.db.MustExec(`create table if not exists webshit_bids ( + id integer primary key autoincrement, user string, title string, url string, bid integer, - created integer - )`); err != nil { - log.Fatal().Err(err) - } - if _, err := w.db.Exec(`create table if not exists webshit_balances ( + placed integer + )`) + w.db.MustExec(`create table if not exists webshit_balances ( user string primary key, balance int, score int - )`); err != nil { - log.Fatal().Err(err) - } + )`) } -func (w *Webshit) Check() (map[string]WeeklyResult, error) { +func (w *Webshit) Check() ([]WeeklyResult, error) { stories, published, err := w.GetWeekly() if err != nil { return nil, err } var bids []Bid - if err = w.db.Get(&bids, `select * from webshit_bids where created < ?`, + if err = w.db.Select(&bids, `select user,title,url,bid from webshit_bids where placed < ?`, published.Unix()); err != nil { return nil, err } // Assuming no bids earlier than the weekly means there hasn't been a new weekly if len(bids) == 0 { - return nil, nil + return nil, fmt.Errorf("there are no bids against the current ngate post") } storyMap := map[string]Story{} @@ -94,7 +102,7 @@ func (w *Webshit) Check() (map[string]WeeklyResult, error) { } // Delete all those bids - if _, err = w.db.Exec(`delete from webshit_bids where created < ?`, + if _, err = w.db.Exec(`delete from webshit_bids where placed < ?`, published.Unix()); err != nil { return nil, err } @@ -107,10 +115,11 @@ func (w *Webshit) Check() (map[string]WeeklyResult, error) { return wr, nil } -func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) map[string]WeeklyResult { +func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) []WeeklyResult { wr := map[string]WeeklyResult{} for _, b := range bids { win, loss := 0, 0 + score := w.GetScore(b.User) if s, ok := storyMap[b.Title]; ok { log.Info().Interface("story", s).Msg("won bid") win = b.Bid @@ -120,17 +129,19 @@ func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) map[string]We } if res, ok := wr[b.User]; !ok { wr[b.User] = WeeklyResult{ - User: b.User, - Won: win, - Lost: loss, + User: b.User, + Won: win, + Lost: loss, + Score: score + win - loss, } } else { - res.Won = win - res.Lost = loss + res.Won += win + res.Lost += loss + res.Score += win - loss wr[b.User] = res } } - return wr + return wrMapToSlice(wr) } // GetHeadlines will return the current possible news headlines for bidding @@ -165,7 +176,7 @@ func (w *Webshit) GetWeekly() ([]Story, *time.Time, error) { return nil, nil, fmt.Errorf("no webshit weekly found") } - published := feed.PublishedParsed + published := feed.Items[0].PublishedParsed buf := bytes.NewBufferString(feed.Items[0].Description) doc, err := goquery.NewDocumentFromReader(buf) @@ -197,6 +208,34 @@ func (w *Webshit) GetBalance(user string) int { return balance } +func (w *Webshit) GetScore(user string) int { + q := `select score from webshit_balances where user=?` + var score int + err := w.db.Get(&score, q, user) + if err != nil { + return 0 + } + return score +} + +func (w *Webshit) GetAllBids() ([]Bid, error) { + var bids []Bid + err := w.db.Select(&bids, `select * from webshit_bids`) + if err != nil { + return nil, err + } + return bids, nil +} + +func (w *Webshit) GetAllBalances() ([]Balance, error) { + var balances []Balance + err := w.db.Select(&balances, `select * from webshit_balances`) + if err != nil { + return nil, err + } + return balances, nil +} + // Bid allows a user to place a bid on a particular story func (w *Webshit) Bid(user string, amount int, URL string) error { bal := w.GetBalance(user) @@ -209,14 +248,15 @@ func (w *Webshit) Bid(user string, amount int, URL string) error { } tx := w.db.MustBegin() - _, err = tx.Exec(`insert into webshit_bids (user,title,url,bid,created) values (?,?,?,?,?)`, + _, err = tx.Exec(`insert into webshit_bids (user,title,url,bid,placed) values (?,?,?,?,?)`, user, story.Title, story.URL, amount, time.Now().Unix()) if err != nil { tx.Rollback() return err } - _, err = tx.Exec(`update webshit_balances set balance=? where user=?`, - bal-amount, user) + q := `insert into webshit_balances (user,balance,score) values (?,?,0) + on conflict(user) do update set balance=?` + _, err = tx.Exec(q, user, bal-amount, bal-amount) if err != nil { tx.Rollback() return err @@ -259,11 +299,11 @@ func (w *Webshit) getStoryByURL(URL string) (Story, error) { }, nil } -func (w *Webshit) updateScores(results map[string]WeeklyResult) error { +func (w *Webshit) updateScores(results []WeeklyResult) error { tx := w.db.MustBegin() for _, res := range results { - if _, err := tx.Exec(`update webshit_balances set score=score+? where user=?`, - res.Won-res.Lost, res.User); err != nil { + if _, err := tx.Exec(`update webshit_balances set score=? where user=?`, + res.Score, res.User); err != nil { tx.Rollback() return err } @@ -271,3 +311,11 @@ func (w *Webshit) updateScores(results map[string]WeeklyResult) error { err := tx.Commit() return err } + +func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult { + var out = []WeeklyResult{} + for _, r := range wr { + out = append(out, r) + } + return out +} diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go index 7caddc9..39fbabc 100644 --- a/plugins/newsbid/webshit/webshit_test.go +++ b/plugins/newsbid/webshit/webshit_test.go @@ -17,7 +17,9 @@ func make(t *testing.T) *Webshit { func TestWebshit_GetWeekly(t *testing.T) { w := make(t) - weekly, _, err := w.GetWeekly() + weekly, pub, err := w.GetWeekly() + t.Logf("Pub: %v", pub) + assert.NotNil(t, pub) assert.Nil(t, err) assert.NotEmpty(t, weekly) } From bdfd6060a0d94e06db82a4e49a55a0915a1dca1b Mon Sep 17 00:00:00 2001 From: Chris Sexton Date: Mon, 15 Jul 2019 22:00:19 -0400 Subject: [PATCH 5/5] bid: fix losing story mode; pretty it up a little --- plugins/newsbid/newsbid.go | 8 ++- plugins/newsbid/webshit/webshit.go | 87 ++++++++++++++++++------- plugins/newsbid/webshit/webshit_test.go | 24 +++++++ 3 files changed, 93 insertions(+), 26 deletions(-) diff --git a/plugins/newsbid/newsbid.go b/plugins/newsbid/newsbid.go index d26dfd8..ffdb2fc 100644 --- a/plugins/newsbid/newsbid.go +++ b/plugins/newsbid/newsbid.go @@ -102,8 +102,14 @@ func (p *NewsBid) check(conn bot.Connector, ch string) { return } for _, res := range wr { - msg := fmt.Sprintf("%s: won %d and lost %d for a score of %d", + msg := fmt.Sprintf("%s won %d and lost %d for a score of %d", res.User, res.Won, res.Lost, res.Score) + if len(res.WinningArticles) > 0 { + msg += "\nWinning articles: " + res.WinningArticles.Titles() + } + if len(res.LosingArticles) > 0 { + msg += "\nLosing articles: " + res.LosingArticles.Titles() + } p.bot.Send(conn, bot.Message, ch, msg) } } diff --git a/plugins/newsbid/webshit/webshit.go b/plugins/newsbid/webshit/webshit.go index 2d88bd5..b8e7d6b 100644 --- a/plugins/newsbid/webshit/webshit.go +++ b/plugins/newsbid/webshit/webshit.go @@ -14,8 +14,21 @@ import ( "time" ) +type Config struct { + HNFeed string + HNLimit int + BalanceReferesh int +} + +var DefaultConfig = Config{ + HNFeed: "topstories", + HNLimit: 10, + BalanceReferesh: 100, +} + type Webshit struct { - db *sqlx.DB + db *sqlx.DB + config Config } type Story struct { @@ -23,6 +36,19 @@ type Story struct { URL string } +type Stories []Story + +func (s Stories) Titles() string { + out := "" + for i, v := range s { + if i > 0 { + out += ", " + } + out += v.Title + } + return out +} + type Bid struct { ID int User string @@ -43,14 +69,20 @@ type Balance struct { } type WeeklyResult struct { - User string - Won int - Lost int - Score int + User string + Won int + WinningArticles Stories + Lost int + LosingArticles Stories + Score int } func New(db *sqlx.DB) *Webshit { - w := &Webshit{db} + return NewConfig(db, DefaultConfig) +} + +func NewConfig(db *sqlx.DB, cfg Config) *Webshit { + w := &Webshit{db: db, config: cfg} w.setup() return w } @@ -108,7 +140,8 @@ func (w *Webshit) Check() ([]WeeklyResult, error) { } // Set all balances to 100 - if _, err = w.db.Exec(`update webshit_balances set balance=100`); err != nil { + if _, err = w.db.Exec(`update webshit_balances set balance=?`, + w.config.BalanceReferesh); err != nil { return nil, err } @@ -118,35 +151,39 @@ func (w *Webshit) Check() ([]WeeklyResult, error) { func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) []WeeklyResult { wr := map[string]WeeklyResult{} for _, b := range bids { - win, loss := 0, 0 score := w.GetScore(b.User) - if s, ok := storyMap[b.Title]; ok { - log.Info().Interface("story", s).Msg("won bid") - win = b.Bid - } else { - log.Info().Interface("story", s).Msg("lost bid") - loss = b.Bid - } - if res, ok := wr[b.User]; !ok { + if _, ok := wr[b.User]; !ok { wr[b.User] = WeeklyResult{ User: b.User, - Won: win, - Lost: loss, - Score: score + win - loss, + Won: 0, + Lost: 0, + Score: score, } - } else { - res.Won += win - res.Lost += loss - res.Score += win - loss - wr[b.User] = res } + rec := wr[b.User] + + if s, ok := storyMap[b.Title]; ok { + log.Debug().Interface("story", s).Msg("won bid") + rec.Won += b.Bid + rec.Score += b.Bid + rec.WinningArticles = append(rec.WinningArticles, s) + log.Debug().Interface("story", s).Msg("Appending to winning log") + } else { + log.Debug().Interface("story", s).Msg("lost bid") + rec.Lost += b.Bid + rec.Score -= b.Bid + rec.LosingArticles = append(rec.LosingArticles, Story{Title: b.Title, URL: b.URL}) + log.Debug().Interface("story", s).Msg("Appending to losing log") + } + wr[b.User] = rec + log.Debug().Interface("WR User", wr[b.User]).Str("user", b.User).Msg("setting WR") } return wrMapToSlice(wr) } // GetHeadlines will return the current possible news headlines for bidding func (w *Webshit) GetHeadlines() ([]Story, error) { - news := hacknews.Initializer{Story: "topstories", NbPosts: 10} + news := hacknews.Initializer{Story: w.config.HNFeed, NbPosts: w.config.HNLimit} ids, err := news.GetCodesStory() if err != nil { return nil, err diff --git a/plugins/newsbid/webshit/webshit_test.go b/plugins/newsbid/webshit/webshit_test.go index 39fbabc..92decbc 100644 --- a/plugins/newsbid/webshit/webshit_test.go +++ b/plugins/newsbid/webshit/webshit_test.go @@ -2,12 +2,19 @@ package webshit import ( "github.com/jmoiron/sqlx" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "github.com/stretchr/testify/assert" + "os" "testing" _ "github.com/mattn/go-sqlite3" ) +func init() { + log.Logger = log.Logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) +} + func make(t *testing.T) *Webshit { db := sqlx.MustOpen("sqlite3", "file::memory:?mode=memory&cache=shared") w := New(db) @@ -51,3 +58,20 @@ func TestWebshit_GetBalance(t *testing.T) { actual := w.GetBalance("foo") assert.Equal(t, expected, actual) } + +func TestWebshit_checkBids(t *testing.T) { + w := make(t) + bids := []Bid{ + Bid{User: "foo", Title: "bar", URL: "baz", Bid: 10}, + Bid{User: "foo", Title: "bar2", URL: "baz2", Bid: 10}, + } + storyMap := map[string]Story{ + "bar": Story{Title: "bar", URL: "baz"}, + } + result := w.checkBids(bids, storyMap) + assert.Len(t, result, 1) + if len(result) > 0 { + assert.Len(t, result[0].WinningArticles, 1) + assert.Len(t, result[0].LosingArticles, 1) + } +}