2019-07-15 16:46:24 +00:00
|
|
|
package webshit
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2021-12-20 17:40:10 +00:00
|
|
|
bh "github.com/timshannon/bolthold"
|
2019-07-15 16:46:24 +00:00
|
|
|
"net/url"
|
2019-11-14 15:09:54 +00:00
|
|
|
"strconv"
|
2019-07-15 18:57:23 +00:00
|
|
|
"time"
|
2019-08-13 20:14:48 +00:00
|
|
|
|
2019-11-21 16:59:52 +00:00
|
|
|
"github.com/velour/catbase/plugins/newsbid/webshit/hn"
|
|
|
|
|
2019-08-13 20:14:48 +00:00
|
|
|
"github.com/PuerkitoBio/goquery"
|
|
|
|
"github.com/mmcdole/gofeed"
|
|
|
|
"github.com/rs/zerolog/log"
|
2019-07-15 16:46:24 +00:00
|
|
|
)
|
|
|
|
|
2019-07-16 02:00:19 +00:00
|
|
|
type Config struct {
|
|
|
|
HNFeed string
|
|
|
|
HNLimit int
|
|
|
|
BalanceReferesh int
|
2020-12-31 19:08:38 +00:00
|
|
|
HouseName string
|
2019-07-16 02:00:19 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 16:46:24 +00:00
|
|
|
type Webshit struct {
|
2021-12-20 17:40:10 +00:00
|
|
|
store *bh.Store
|
2019-07-16 02:00:19 +00:00
|
|
|
config Config
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 17:39:40 +00:00
|
|
|
type Bid struct {
|
2021-12-20 17:40:10 +00:00
|
|
|
ID int64 `boltholdid:"ID"`
|
2019-11-21 16:59:52 +00:00
|
|
|
User string
|
|
|
|
Title string
|
|
|
|
URL string
|
2019-12-20 18:31:05 +00:00
|
|
|
HNID int `db:"hnid"`
|
2019-11-21 16:59:52 +00:00
|
|
|
Bid int
|
2019-12-20 18:31:05 +00:00
|
|
|
BidStr string
|
2019-11-22 16:51:48 +00:00
|
|
|
PlacedScore int `db:"placed_score"`
|
|
|
|
ProcessedScore int `db:"processed_score"`
|
2021-12-20 17:40:10 +00:00
|
|
|
Placed time.Time
|
|
|
|
Processed bool
|
2019-07-15 20:55:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type Balance struct {
|
2021-12-20 17:40:10 +00:00
|
|
|
User string `boltholdid:"User"`
|
2019-07-15 20:55:35 +00:00
|
|
|
Balance int
|
|
|
|
Score int
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
|
2020-03-11 16:25:34 +00:00
|
|
|
type Balances []Balance
|
|
|
|
|
|
|
|
func (b Balances) Len() int { return len(b) }
|
|
|
|
func (b Balances) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
2020-03-11 17:11:22 +00:00
|
|
|
func (b Balances) Less(i, j int) bool { return b[i].Score > b[j].Score }
|
2020-03-11 16:25:34 +00:00
|
|
|
|
2019-07-15 18:57:23 +00:00
|
|
|
type WeeklyResult struct {
|
2019-07-16 02:00:19 +00:00
|
|
|
User string
|
|
|
|
Won int
|
2019-11-21 16:59:52 +00:00
|
|
|
WinningArticles hn.Items
|
|
|
|
LosingArticles hn.Items
|
2019-07-16 02:00:19 +00:00
|
|
|
Score int
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2021-12-20 17:40:10 +00:00
|
|
|
func NewConfig(store *bh.Store, cfg Config) *Webshit {
|
|
|
|
w := &Webshit{store: store, config: cfg}
|
2019-07-15 16:46:24 +00:00
|
|
|
return w
|
|
|
|
}
|
|
|
|
|
2019-11-22 16:51:48 +00:00
|
|
|
func (w *Webshit) Check(last int64) ([]WeeklyResult, int64, error) {
|
2019-07-15 18:57:23 +00:00
|
|
|
stories, published, err := w.GetWeekly()
|
|
|
|
if err != nil {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if published.Unix() <= last {
|
2019-12-20 18:31:05 +00:00
|
|
|
log.Debug().Msgf("No new ngate: %v vs %v", published.Unix(), last)
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, fmt.Errorf("no new ngate")
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var bids []Bid
|
2021-12-20 17:40:10 +00:00
|
|
|
if err = w.store.Find(&bids, bh.Where("processed").Eq(false)); err != nil {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2019-12-20 18:31:05 +00:00
|
|
|
log.Debug().
|
|
|
|
Interface("bids", bids).
|
|
|
|
Interface("ngate", stories).
|
|
|
|
Interface("published", published).
|
|
|
|
Msg("checking ngate")
|
|
|
|
|
2019-07-15 18:57:23 +00:00
|
|
|
// Assuming no bids earlier than the weekly means there hasn't been a new weekly
|
|
|
|
if len(bids) == 0 {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, fmt.Errorf("there are no bids against the current ngate post")
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 16:59:52 +00:00
|
|
|
storyMap := map[string]hn.Item{}
|
2019-07-15 18:57:23 +00:00
|
|
|
for _, s := range stories {
|
2019-12-20 18:31:05 +00:00
|
|
|
storyMap[s.URL] = s
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
wr := w.checkBids(bids, storyMap)
|
|
|
|
|
|
|
|
// Update all balance scores in a tx
|
|
|
|
if err := w.updateScores(wr); err != nil {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete all those bids
|
2021-12-20 17:40:10 +00:00
|
|
|
if err = w.store.UpdateMatching(Bid{}, bh.Where("processed").Eq(false), func(record interface{}) error {
|
|
|
|
r := record.(*Bid)
|
|
|
|
r.Processed = true
|
|
|
|
return w.store.Update(r.ID, r)
|
|
|
|
}); err != nil {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Set all balances to 100
|
2021-12-20 17:40:10 +00:00
|
|
|
if err = w.store.UpdateMatching(Balance{}, &bh.Query{}, func(record interface{}) error {
|
|
|
|
r := record.(*Balance)
|
|
|
|
r.Balance = w.config.BalanceReferesh
|
|
|
|
return w.store.Update(r.User, r)
|
|
|
|
}); err != nil {
|
2019-11-22 16:51:48 +00:00
|
|
|
return nil, 0, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2019-11-22 16:51:48 +00:00
|
|
|
return wr, published.Unix(), nil
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 16:59:52 +00:00
|
|
|
func (w *Webshit) checkBids(bids []Bid, storyMap map[string]hn.Item) []WeeklyResult {
|
2019-08-11 19:05:15 +00:00
|
|
|
var wins []Bid
|
2019-08-13 20:14:48 +00:00
|
|
|
total, totalWinning := 0.0, 0.0
|
2019-07-15 18:57:23 +00:00
|
|
|
wr := map[string]WeeklyResult{}
|
2019-08-11 19:05:15 +00:00
|
|
|
|
2020-12-31 19:08:38 +00:00
|
|
|
houseName := w.config.HouseName
|
|
|
|
houseScore := 0
|
|
|
|
|
2019-07-15 18:57:23 +00:00
|
|
|
for _, b := range bids {
|
2021-12-20 17:40:10 +00:00
|
|
|
score := w.GetBalance(b.User).Score
|
2019-07-16 02:00:19 +00:00
|
|
|
if _, ok := wr[b.User]; !ok {
|
2019-07-15 18:57:23 +00:00
|
|
|
wr[b.User] = WeeklyResult{
|
2019-07-15 20:55:35 +00:00
|
|
|
User: b.User,
|
2019-07-16 02:00:19 +00:00
|
|
|
Score: score,
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-07-16 02:00:19 +00:00
|
|
|
}
|
|
|
|
rec := wr[b.User]
|
|
|
|
|
2019-12-20 18:31:05 +00:00
|
|
|
if s, ok := storyMap[b.URL]; ok {
|
2019-08-11 19:05:15 +00:00
|
|
|
wins = append(wins, b)
|
2019-12-20 18:31:05 +00:00
|
|
|
s.Bid = b.BidStr
|
2019-07-16 02:00:19 +00:00
|
|
|
rec.WinningArticles = append(rec.WinningArticles, s)
|
2019-08-13 20:14:48 +00:00
|
|
|
totalWinning += float64(b.Bid)
|
2019-07-15 18:57:23 +00:00
|
|
|
} else {
|
2019-12-20 18:31:05 +00:00
|
|
|
bid := hn.Item{
|
|
|
|
ID: b.HNID,
|
|
|
|
URL: b.URL,
|
|
|
|
Title: b.Title,
|
|
|
|
Bid: b.BidStr,
|
|
|
|
}
|
|
|
|
rec.LosingArticles = append(rec.LosingArticles, bid)
|
2020-12-31 19:08:38 +00:00
|
|
|
houseScore += b.Bid
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-08-13 20:14:48 +00:00
|
|
|
total += float64(b.Bid)
|
2019-07-16 02:00:19 +00:00
|
|
|
wr[b.User] = rec
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-08-11 19:05:15 +00:00
|
|
|
|
|
|
|
for _, b := range wins {
|
|
|
|
rec := wr[b.User]
|
2020-12-31 19:08:38 +00:00
|
|
|
rec.Won += b.Bid
|
|
|
|
rec.Score += b.Bid
|
2019-08-11 19:05:15 +00:00
|
|
|
wr[b.User] = rec
|
|
|
|
}
|
|
|
|
|
2020-12-31 19:08:38 +00:00
|
|
|
wr[houseName] = WeeklyResult{
|
|
|
|
User: houseName,
|
2021-12-20 17:40:10 +00:00
|
|
|
Score: w.GetBalance(houseName).Score + houseScore,
|
2020-12-31 19:08:38 +00:00
|
|
|
Won: houseScore,
|
|
|
|
}
|
|
|
|
|
2019-07-15 20:55:35 +00:00
|
|
|
return wrMapToSlice(wr)
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 16:46:24 +00:00
|
|
|
// GetWeekly will return the headlines in the last webshit weekly report
|
2019-11-21 16:59:52 +00:00
|
|
|
func (w *Webshit) GetWeekly() (hn.Items, *time.Time, error) {
|
2019-07-15 16:46:24 +00:00
|
|
|
fp := gofeed.NewParser()
|
|
|
|
feed, err := fp.ParseURL("http://n-gate.com/hackernews/index.rss")
|
|
|
|
if err != nil {
|
2019-07-15 18:57:23 +00:00
|
|
|
return nil, nil, err
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
if len(feed.Items) <= 0 {
|
2019-07-15 18:57:23 +00:00
|
|
|
return nil, nil, fmt.Errorf("no webshit weekly found")
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 20:55:35 +00:00
|
|
|
published := feed.Items[0].PublishedParsed
|
2019-07-15 18:57:23 +00:00
|
|
|
|
2019-07-15 16:46:24 +00:00
|
|
|
buf := bytes.NewBufferString(feed.Items[0].Description)
|
|
|
|
doc, err := goquery.NewDocumentFromReader(buf)
|
|
|
|
if err != nil {
|
2019-07-15 18:57:23 +00:00
|
|
|
return nil, nil, err
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
|
2019-11-21 16:59:52 +00:00
|
|
|
var items hn.Items
|
2019-07-15 16:46:24 +00:00
|
|
|
doc.Find(".storylink").Each(func(i int, s *goquery.Selection) {
|
2019-11-22 16:51:48 +00:00
|
|
|
url, err := url.Parse(s.SiblingsFiltered(".small").First().Find("a").AttrOr("href", ""))
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("Could not parse URL from ngate")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
id, _ := strconv.Atoi(url.Query().Get("id"))
|
|
|
|
item, err := hn.GetItem(id)
|
|
|
|
if err != nil {
|
|
|
|
log.Error().Err(err).Msg("Could not get story from ngate")
|
|
|
|
return
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-11-22 16:51:48 +00:00
|
|
|
items = append(items, item)
|
2019-07-15 16:46:24 +00:00
|
|
|
})
|
|
|
|
|
2019-07-15 18:57:23 +00:00
|
|
|
return items, published, nil
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetBalances returns the current balance for all known users
|
|
|
|
// Any unknown user has a default balance on their first bid
|
2021-12-20 17:40:10 +00:00
|
|
|
func (w *Webshit) GetBalance(user string) Balance {
|
|
|
|
var balance Balance
|
|
|
|
err := w.store.Get(user, &balance)
|
2019-07-15 17:39:40 +00:00
|
|
|
if err != nil {
|
2021-12-20 17:40:10 +00:00
|
|
|
return Balance{
|
|
|
|
User: user,
|
|
|
|
Balance: 100,
|
|
|
|
Score: 0,
|
|
|
|
}
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
return balance
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
|
|
|
|
2019-07-15 20:55:35 +00:00
|
|
|
func (w *Webshit) GetAllBids() ([]Bid, error) {
|
|
|
|
var bids []Bid
|
2021-12-20 17:40:10 +00:00
|
|
|
err := w.store.Find(&bids, bh.Where("processed").Eq(false))
|
2019-07-15 20:55:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return bids, nil
|
|
|
|
}
|
|
|
|
|
2020-03-11 16:25:34 +00:00
|
|
|
func (w *Webshit) GetAllBalances() (Balances, error) {
|
|
|
|
var balances Balances
|
2021-12-20 17:40:10 +00:00
|
|
|
err := w.store.Find(&balances, &bh.Query{})
|
2019-07-15 20:55:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return balances, nil
|
|
|
|
}
|
|
|
|
|
2019-07-15 16:46:24 +00:00
|
|
|
// Bid allows a user to place a bid on a particular story
|
2019-12-20 18:31:05 +00:00
|
|
|
func (w *Webshit) Bid(user string, amount int, bidStr, URL string) (Bid, error) {
|
2021-12-20 17:40:10 +00:00
|
|
|
bal := w.GetBalance(user).Balance
|
2019-07-18 18:57:24 +00:00
|
|
|
if amount < 0 {
|
|
|
|
return Bid{}, fmt.Errorf("cannot bid less than 0")
|
|
|
|
}
|
2019-07-15 17:39:40 +00:00
|
|
|
if bal < amount {
|
2019-07-16 19:55:38 +00:00
|
|
|
return Bid{}, fmt.Errorf("cannot bid more than balance, %d", bal)
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
story, err := w.getStoryByURL(URL)
|
|
|
|
if err != nil {
|
2019-07-16 19:55:38 +00:00
|
|
|
return Bid{}, err
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
|
2021-12-20 17:40:10 +00:00
|
|
|
bid := Bid{
|
|
|
|
User: user,
|
|
|
|
Title: story.Title,
|
|
|
|
URL: story.URL,
|
|
|
|
Placed: time.Now(),
|
|
|
|
}
|
2019-07-16 19:55:38 +00:00
|
|
|
|
2021-12-20 17:40:10 +00:00
|
|
|
err = w.store.Insert(Bid{}, bid)
|
2019-07-15 18:57:23 +00:00
|
|
|
if err != nil {
|
2019-07-16 19:55:38 +00:00
|
|
|
return Bid{}, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2021-12-20 17:40:10 +00:00
|
|
|
|
|
|
|
err = w.store.Upsert(user, bal)
|
2019-07-15 18:57:23 +00:00
|
|
|
if err != nil {
|
2019-07-16 19:55:38 +00:00
|
|
|
return Bid{}, err
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-07-15 17:39:40 +00:00
|
|
|
|
2021-12-20 17:40:10 +00:00
|
|
|
return bid, nil
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// getStoryByURL scrapes the URL for a title
|
2019-11-21 16:59:52 +00:00
|
|
|
func (w *Webshit) getStoryByURL(URL string) (hn.Item, error) {
|
2019-07-15 17:39:40 +00:00
|
|
|
u, err := url.Parse(URL)
|
|
|
|
if err != nil {
|
2019-11-21 16:59:52 +00:00
|
|
|
return hn.Item{}, err
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
|
|
|
if u.Host != "news.ycombinator.com" {
|
2019-11-21 16:59:52 +00:00
|
|
|
return hn.Item{}, fmt.Errorf("expected HN link")
|
2019-07-15 17:39:40 +00:00
|
|
|
}
|
2019-11-21 17:13:07 +00:00
|
|
|
id, err := strconv.Atoi(u.Query().Get("id"))
|
|
|
|
if id == 0 || err != nil {
|
|
|
|
return hn.Item{}, fmt.Errorf("invalid item ID")
|
|
|
|
}
|
2019-11-21 16:59:52 +00:00
|
|
|
return hn.GetItem(id)
|
2019-07-15 16:46:24 +00:00
|
|
|
}
|
2019-07-15 18:57:23 +00:00
|
|
|
|
2019-07-15 20:55:35 +00:00
|
|
|
func (w *Webshit) updateScores(results []WeeklyResult) error {
|
2019-07-15 18:57:23 +00:00
|
|
|
for _, res := range results {
|
2021-12-20 17:40:10 +00:00
|
|
|
bal := w.GetBalance(res.User)
|
|
|
|
bal.Score = res.Score
|
|
|
|
if err := w.store.Insert(res.User, bal); err != nil {
|
2019-07-15 18:57:23 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
2021-12-20 17:40:10 +00:00
|
|
|
return nil
|
2019-07-15 18:57:23 +00:00
|
|
|
}
|
2019-07-15 20:55:35 +00:00
|
|
|
|
|
|
|
func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult {
|
|
|
|
var out = []WeeklyResult{}
|
|
|
|
for _, r := range wr {
|
|
|
|
out = append(out, r)
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|