catbase/plugins/newsbid/webshit/webshit.go

321 lines
7.1 KiB
Go
Raw Normal View History

package webshit
import (
"bytes"
"fmt"
2021-12-20 17:40:10 +00:00
bh "github.com/timshannon/bolthold"
"net/url"
"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"
)
type Config struct {
HNFeed string
HNLimit int
BalanceReferesh int
HouseName string
}
type Webshit struct {
2021-12-20 17:40:10 +00:00
store *bh.Store
config Config
}
2019-07-15 17:39:40 +00:00
type Bid struct {
2021-12-21 19:08:20 +00:00
ID int64 `boltholdKey:"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
2021-12-22 03:36:08 +00:00
Processed time.Time
}
type Balance struct {
2021-12-21 19:08:20 +00:00
User string `boltholdKey:"User"`
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 {
User string
Won int
2019-11-21 16:59:52 +00:00
WinningArticles hn.Items
LosingArticles hn.Items
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}
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-21 04:31:19 +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-21 04:31:19 +00:00
if err = w.store.UpdateMatching(Bid{}, bh.Where("Processed").Eq(false), func(record interface{}) error {
2021-12-20 17:40:10 +00:00
r := record.(*Bid)
2021-12-22 03:36:08 +00:00
r.Processed = time.Now()
2021-12-20 17:40:10 +00:00
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 {
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{}
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
if _, ok := wr[b.User]; !ok {
2019-07-15 18:57:23 +00:00
wr[b.User] = WeeklyResult{
User: b.User,
Score: score,
2019-07-15 18:57:23 +00:00
}
}
rec := wr[b.User]
2019-12-20 18:31:05 +00:00
if s, ok := storyMap[b.URL]; ok {
wins = append(wins, b)
2019-12-20 18:31:05 +00:00
s.Bid = b.BidStr
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)
houseScore += b.Bid
2019-07-15 18:57:23 +00:00
}
2019-08-13 20:14:48 +00:00
total += float64(b.Bid)
wr[b.User] = rec
2019-07-15 18:57:23 +00:00
}
for _, b := range wins {
rec := wr[b.User]
rec.Won += b.Bid
rec.Score += b.Bid
wr[b.User] = rec
}
wr[houseName] = WeeklyResult{
User: houseName,
2021-12-20 17:40:10 +00:00
Score: w.GetBalance(houseName).Score + houseScore,
Won: houseScore,
}
return wrMapToSlice(wr)
2019-07-15 18:57:23 +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) {
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
}
if len(feed.Items) <= 0 {
2019-07-15 18:57:23 +00:00
return nil, nil, fmt.Errorf("no webshit weekly found")
}
published := feed.Items[0].PublishedParsed
2019-07-15 18:57:23 +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-11-21 16:59:52 +00:00
var items hn.Items
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 18:57:23 +00:00
return items, published, nil
}
// 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
}
func (w *Webshit) GetAllBids() ([]Bid, error) {
var bids []Bid
2021-12-21 04:31:19 +00:00
err := w.store.Find(&bids, bh.Where("Processed").Eq(false))
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{})
if err != nil {
return nil, err
}
return balances, nil
}
// 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 {
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 {
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(),
}
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 {
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 {
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 18:57:23 +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
}
func wrMapToSlice(wr map[string]WeeklyResult) []WeeklyResult {
var out = []WeeklyResult{}
for _, r := range wr {
out = append(out, r)
}
return out
}