package aoc import ( "encoding/json" "fmt" "io" "net/http" "strconv" "time" ) type AoC struct { session string boardId string boards map[int]*Leaderboard sessionErr bool } func NewAoC(boardId, session string) (*AoC, error) { if boardId == "" { return nil, BoardIdRequiredError } if session == "" { return nil, SessionRequiredError } return &AoC{ session: session, boardId: boardId, boards: make(map[int]*Leaderboard), }, nil } func (a *AoC) GetSession() string { return a.session } func (a *AoC) SetSession(session string) { a.session = session a.sessionErr = false } func (a *AoC) NeedsNewSession() bool { return a.sessionErr } func (a *AoC) SetCachedLeaderboard(l *Leaderboard) error { yr, err := strconv.Atoi(l.Event) if err != nil { return InvalidYearError } a.boards[yr] = l return nil } func (a *AoC) GetCachedLeaderboard(year int) (*Leaderboard, error) { if year < 2015 || year > time.Now().Year() { return nil, InvalidYearError } if board, ok := a.boards[year]; ok { return board, nil } return nil, LeaderboardNotCachedError } func (a *AoC) GetLeaderboard(year int) (*Leaderboard, error) { if year < 2015 || year > time.Now().Year() { return nil, InvalidYearError } if a.sessionErr { return nil, SessionError } if board, ok := a.boards[year]; ok { if time.Since(board.LastFetch) < (time.Minute * 15) { 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" { a.sessionErr = true return nil, SessionError } 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) if err != nil { return nil, err } req.Header.Add("Cookie", "session="+a.session) resp, err = client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err = io.ReadAll(resp.Body) if err != nil { return nil, err } strBody := string(body) err = json.Unmarshal([]byte(strBody), &leaderboard) if err != nil { if err.Error() == "invalid character '<' looking for beginning of value" { a.sessionErr = true return nil, SessionError } return nil, fmt.Errorf("error parsing board: %w", 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 int `json:"owner_id"` Event string `json:"event"` Members map[string]Member `json:"members"` LastFetch time.Time } type Member struct { ID int `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 } func (m *Member) UnmarshalJSON(data []byte) error { var v map[string]interface{} var err error if err = json.Unmarshal(data, &v); err != nil { return err } m.ID = int(v["id"].(float64)) m.Name = v["name"].(string) m.Stars = int(v["stars"].(float64)) switch v["last_star_ts"].(type) { case float64: m.RawStarTs = fmt.Sprintf("%d", int(v["last_star_ts"].(float64))) case int: m.RawStarTs = strconv.Itoa(v["last_star_ts"].(int)) case string: m.RawStarTs = v["last_star_ts"].(string) default: m.RawStarTs = "0" } starTs, err := strconv.ParseInt(m.RawStarTs, 10, 64) if err != nil { starTs = 0 } m.LastStarTs = time.Unix(starTs, 0) return nil }