package aoc import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "strconv" "strings" "time" ) type AoC struct { session string boardId string boards map[int]*Leaderboard } func NewAoC(boardId, session string) (*AoC, error) { if boardId == "" { return nil, errors.New("Board ID is required") } if session == "" { return nil, errors.New("Session key is required") } return &AoC{ session: session, boardId: boardId, boards: make(map[int]*Leaderboard), }, nil } func (a *AoC) GetCachedLeaderboard(year int) (*Leaderboard, error) { if year < 2015 || year > time.Now().Year() { return nil, errors.New("Invalid Year") } if board, ok := a.boards[year]; ok { return board, nil } return nil, errors.New(fmt.Sprintf("Leaderboard (%d) is not cached", year)) } func (a *AoC) GetLeaderboard(year int) (*Leaderboard, error) { if year < 2015 || year > time.Now().Year() { return nil, errors.New("Invalid Year") } if board, ok := a.boards[year]; ok { if time.Since(board.LastFetch) < (time.Minute * 10) { return board, nil } } var err error a.boards[year], err = a.fetchLeaderboard(year) if err != nil { if err.Error() == "invalid character '<' looking for beginning of value" { return nil, errors.New("Invalid Session Cookie") } return nil, err } return a.boards[year], nil } func (a *AoC) fetchLeaderboard(year int) (*Leaderboard, error) { var err error var req *http.Request var resp *http.Response var body []byte leaderboard := new(Leaderboard) leaderboard.Members = make(map[string]Member) client := &http.Client{} boardString := fmt.Sprintf("https://adventofcode.com/%d/leaderboard/private/view/%s.json", year, a.boardId) req, err = http.NewRequest("GET", boardString, nil) req.Header.Add("Cookie", "session="+a.session) resp, err = client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err = ioutil.ReadAll(resp.Body) if err != nil { return nil, err } strBody := string(body) strBody = strings.ReplaceAll(strBody, "\"last_star_ts\":0", "\"last_star_ts\":\"0\"") err = json.Unmarshal([]byte(strBody), &leaderboard) if err != nil { return nil, err } for k, mbr := range leaderboard.Members { starTs, err := strconv.ParseInt(mbr.RawStarTs, 10, 64) if err != nil { continue } mbr.LastStarTs = time.Unix(starTs, 0) leaderboard.Members[k] = mbr } leaderboard.LastFetch = time.Now() a.boards[year] = leaderboard return leaderboard, err } type Leaderboard struct { OwnerID string `json:"owner_id"` Event string `json:"event"` Members map[string]Member `json:"members"` LastFetch time.Time } type Member struct { ID string `json:"id"` Name string `json:"name"` Stars int `json:"stars"` RawStarTs string `json:"last_star_ts"` LocalScore int `json:"local_score"` GlobalScore int `json:"global_score"` LastStarTs time.Time SlackID string } type ByLocalScore []Member func (a ByLocalScore) Len() int { return len(a) } func (a ByLocalScore) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByLocalScore) Less(i, j int) bool { return a[i].LocalScore < a[j].LocalScore } type ByStars []Member func (a ByStars) Len() int { return len(a) } func (a ByStars) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByStars) Less(i, j int) bool { return a[i].Stars < a[j].Stars } type ByStarsThenScore []Member func (a ByStarsThenScore) Len() int { return len(a) } func (a ByStarsThenScore) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a ByStarsThenScore) Less(i, j int) bool { if a[i].Stars == a[j].Stars { return a[i].LocalScore < a[j].LocalScore } return a[i].Stars < a[j].Stars }