mirror of https://github.com/velour/catbase.git
commit
aeaf102479
5
go.mod
5
go.mod
|
@ -1,7 +1,8 @@
|
||||||
module github.com/velour/catbase
|
module github.com/velour/catbase
|
||||||
|
|
||||||
require (
|
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/armon/go-radix v1.0.0 // indirect
|
||||||
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
|
github.com/chrissexton/leftpad v0.0.0-20181207133115-1e93189d2fff
|
||||||
github.com/go-sql-driver/mysql v1.4.1 // indirect
|
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/chat v0.0.0-20180713122344-fd1d1606cb89
|
||||||
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
|
github.com/velour/velour v0.0.0-20160303155839-8e090e68d158
|
||||||
golang.org/x/exp v0.0.0-20190321205749-f0864edee7f3 // indirect
|
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
|
gonum.org/v1/gonum v0.0.0-20190325211145-e42c1265cdd5 // indirect
|
||||||
google.golang.org/appengine v1.4.0 // indirect
|
google.golang.org/appengine v1.4.0 // indirect
|
||||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||||
|
|
6
go.sum
6
go.sum
|
@ -1,6 +1,8 @@
|
||||||
github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE=
|
github.com/AlekSi/pointer v1.0.0 h1:KWCWzsvFxNLcmM5XmiqHsGTTsuwZMsLFwWF9Y+//bNE=
|
||||||
github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QKVOSOA8=
|
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/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 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
|
||||||
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
|
||||||
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
|
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-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-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-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-20190606173856-1492cefac77f h1:IWHgpgFqnL5AhBUBZSgBdjl2vkQUEzcY+JNKWfcgAU0=
|
||||||
golang.org/x/net v0.0.0-20190326090315-15845e8f865b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
|
|
2
main.go
2
main.go
|
@ -5,6 +5,7 @@ package main
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"github.com/velour/catbase/plugins/cli"
|
"github.com/velour/catbase/plugins/cli"
|
||||||
|
"github.com/velour/catbase/plugins/newsbid"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
@ -124,6 +125,7 @@ func main() {
|
||||||
b.AddPlugin(nerdepedia.New(b))
|
b.AddPlugin(nerdepedia.New(b))
|
||||||
b.AddPlugin(tldr.New(b))
|
b.AddPlugin(tldr.New(b))
|
||||||
b.AddPlugin(stock.New(b))
|
b.AddPlugin(stock.New(b))
|
||||||
|
b.AddPlugin(newsbid.New(b))
|
||||||
b.AddPlugin(cli.New(b))
|
b.AddPlugin(cli.New(b))
|
||||||
// catches anything left, will always return true
|
// catches anything left, will always return true
|
||||||
b.AddPlugin(fact.New(b))
|
b.AddPlugin(fact.New(b))
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,358 @@
|
||||||
|
package webshit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/PaulRosset/go-hacknews"
|
||||||
|
"github.com/PuerkitoBio/goquery"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/mmcdole/gofeed"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"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
|
||||||
|
config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Story struct {
|
||||||
|
Title string
|
||||||
|
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
|
||||||
|
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
|
||||||
|
WinningArticles Stories
|
||||||
|
Lost int
|
||||||
|
LosingArticles Stories
|
||||||
|
Score int
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sqlx.DB) *Webshit {
|
||||||
|
return NewConfig(db, DefaultConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig(db *sqlx.DB, cfg Config) *Webshit {
|
||||||
|
w := &Webshit{db: db, config: cfg}
|
||||||
|
w.setup()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup will create any necessary SQL tables and populate them with minimal data
|
||||||
|
func (w *Webshit) setup() {
|
||||||
|
w.db.MustExec(`create table if not exists webshit_bids (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
user string,
|
||||||
|
title string,
|
||||||
|
url string,
|
||||||
|
bid integer,
|
||||||
|
placed integer
|
||||||
|
)`)
|
||||||
|
w.db.MustExec(`create table if not exists webshit_balances (
|
||||||
|
user string primary key,
|
||||||
|
balance int,
|
||||||
|
score int
|
||||||
|
)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webshit) Check() ([]WeeklyResult, error) {
|
||||||
|
stories, published, err := w.GetWeekly()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bids []Bid
|
||||||
|
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, fmt.Errorf("there are no bids against the current ngate post")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 placed < ?`,
|
||||||
|
published.Unix()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set all balances to 100
|
||||||
|
if _, err = w.db.Exec(`update webshit_balances set balance=?`,
|
||||||
|
w.config.BalanceReferesh); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webshit) checkBids(bids []Bid, storyMap map[string]Story) []WeeklyResult {
|
||||||
|
wr := map[string]WeeklyResult{}
|
||||||
|
for _, b := range bids {
|
||||||
|
score := w.GetScore(b.User)
|
||||||
|
if _, ok := wr[b.User]; !ok {
|
||||||
|
wr[b.User] = WeeklyResult{
|
||||||
|
User: b.User,
|
||||||
|
Won: 0,
|
||||||
|
Lost: 0,
|
||||||
|
Score: score,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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: w.config.HNFeed, NbPosts: w.config.HNLimit}
|
||||||
|
ids, err := news.GetCodesStory()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
posts, err := news.GetPostStory(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var stories []Story
|
||||||
|
for _, p := range posts {
|
||||||
|
stories = append(stories, Story{
|
||||||
|
Title: p.Title,
|
||||||
|
URL: p.Url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stories, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWeekly will return the headlines in the last webshit weekly report
|
||||||
|
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, nil, err
|
||||||
|
}
|
||||||
|
if len(feed.Items) <= 0 {
|
||||||
|
return nil, nil, fmt.Errorf("no webshit weekly found")
|
||||||
|
}
|
||||||
|
|
||||||
|
published := feed.Items[0].PublishedParsed
|
||||||
|
|
||||||
|
buf := bytes.NewBufferString(feed.Items[0].Description)
|
||||||
|
doc, err := goquery.NewDocumentFromReader(buf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []Story
|
||||||
|
doc.Find(".storylink").Each(func(i int, s *goquery.Selection) {
|
||||||
|
story := Story{
|
||||||
|
Title: s.Find("a").Text(),
|
||||||
|
URL: s.Find("a").AttrOr("src", ""),
|
||||||
|
}
|
||||||
|
items = append(items, story)
|
||||||
|
})
|
||||||
|
|
||||||
|
return items, published, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBalances returns the current balance for all known users
|
||||||
|
// Any unknown user has a default balance on their first bid
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if bal < amount {
|
||||||
|
return fmt.Errorf("cannot bid more than balance, %d", bal)
|
||||||
|
}
|
||||||
|
story, err := w.getStoryByURL(URL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := w.db.MustBegin()
|
||||||
|
_, 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Webshit) updateScores(results []WeeklyResult) error {
|
||||||
|
tx := w.db.MustBegin()
|
||||||
|
for _, res := range results {
|
||||||
|
if _, err := tx.Exec(`update webshit_balances set score=? where user=?`,
|
||||||
|
res.Score, res.User); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
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)
|
||||||
|
assert.Equal(t, w.db, db)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWebshit_GetWeekly(t *testing.T) {
|
||||||
|
w := make(t)
|
||||||
|
weekly, pub, err := w.GetWeekly()
|
||||||
|
t.Logf("Pub: %v", pub)
|
||||||
|
assert.NotNil(t, pub)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue