Scan every 15 minutes, run until stopped
This commit is contained in:
1
go.mod
1
go.mod
@ -5,5 +5,6 @@ go 1.21.4
|
|||||||
require (
|
require (
|
||||||
github.com/goccy/go-json v0.10.2
|
github.com/goccy/go-json v0.10.2
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/valyala/fastjson v1.6.4
|
github.com/valyala/fastjson v1.6.4
|
||||||
)
|
)
|
||||||
|
2
go.sum
2
go.sum
@ -2,5 +2,7 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
|||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||||
|
117
main.go
117
main.go
@ -10,11 +10,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/valyala/fastjson"
|
"github.com/valyala/fastjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,7 +37,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type completionPartData struct {
|
type completionPartData struct {
|
||||||
GetStarTimestamp int64 `json:"get_star_ts"`
|
GotStarAt int64 `json:"get_star_ts"`
|
||||||
StarIndex int64 `json:"star_index"`
|
StarIndex int64 `json:"star_index"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,17 +53,20 @@ type memberData struct {
|
|||||||
LocalScore int `json:"local_score"`
|
LocalScore int `json:"local_score"`
|
||||||
GlobalScore int `json:"global_score"`
|
GlobalScore int `json:"global_score"`
|
||||||
Stars int `json:"stars"`
|
Stars int `json:"stars"`
|
||||||
LastStarTs int `json:"last_star_ts"`
|
LastStarTimestamp int `json:"last_star_ts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type leaderboardData struct {
|
type leaderboardData struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
Members []memberData `json:"-"`
|
Members []memberData `json:"-"`
|
||||||
OwnerId int `json:"owner_id"`
|
OwnerID int `json:"owner_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
fmt.Println("Started AOC leaderboard scanner.")
|
||||||
|
|
||||||
dotenvErr := godotenv.Load()
|
dotenvErr := godotenv.Load()
|
||||||
if dotenvErr != nil {
|
if dotenvErr != nil {
|
||||||
log.Fatal("Error loading .env file")
|
log.Fatal("Error loading .env file")
|
||||||
@ -112,21 +118,25 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Since(time.Unix(lastRead, 0)) < time.Minute*15 {
|
refresh := func() {
|
||||||
fmt.Println("Too soon since the last request; doing nothing")
|
fmt.Println("Scanning for new leaderboard data...")
|
||||||
|
|
||||||
|
// the website requests no more than every 15mins, but this gives us a little slop for cron jobs
|
||||||
|
if time.Since(time.Unix(lastRead, 0)) < time.Minute*14 {
|
||||||
|
log.Println("Too soon since the last request; doing nothing")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currBody := lastBody
|
|
||||||
|
|
||||||
currBody, downloadErr := downloadLeaderboardData(*yearArg, leaderboardID, session)
|
currBody, downloadErr := downloadLeaderboardData(*yearArg, leaderboardID, session)
|
||||||
if downloadErr != nil {
|
if downloadErr != nil {
|
||||||
log.Fatalln("Error downloading leaderboard data:", downloadErr)
|
log.Println("Error downloading leaderboard data:", downloadErr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRead = time.Now().Unix()
|
lastRead = time.Now().Unix()
|
||||||
jsonBytes, marshalErr := json.Marshal(map[string]any{"last_read": lastRead, "last_body": string(currBody)})
|
jsonBytes, marshalErr := json.Marshal(map[string]any{"last_read": lastRead, "last_body": string(currBody)})
|
||||||
if marshalErr != nil {
|
if marshalErr != nil {
|
||||||
log.Println("Failed to marshal last-read data into json:", marshalErr)
|
log.Println("Failed to marshal last-read data into json. Data:", string(jsonBytes), "- error:", marshalErr)
|
||||||
} else {
|
} else {
|
||||||
writeErr := os.WriteFile(".cache.json", jsonBytes, 0644)
|
writeErr := os.WriteFile(".cache.json", jsonBytes, 0644)
|
||||||
if writeErr != nil {
|
if writeErr != nil {
|
||||||
@ -136,18 +146,20 @@ func main() {
|
|||||||
|
|
||||||
lastLeaderboard, lastLeaderboardErr := buildLeaderboard(lastBody)
|
lastLeaderboard, lastLeaderboardErr := buildLeaderboard(lastBody)
|
||||||
if lastLeaderboardErr != nil {
|
if lastLeaderboardErr != nil {
|
||||||
log.Fatalln("Error building leaderboard from cached body:", lastLeaderboardErr)
|
log.Println("Error building leaderboard from cached body:", lastLeaderboardErr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
leaderboard, leaderboardErr := buildLeaderboard(currBody)
|
leaderboard, leaderboardErr := buildLeaderboard(currBody)
|
||||||
if leaderboardErr != nil {
|
if leaderboardErr != nil {
|
||||||
log.Fatalln("Error building leaderboard from downloaded body:", leaderboardErr)
|
log.Println("Error building leaderboard from downloaded body:", leaderboardErr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, member := range leaderboard.Members {
|
for _, member := range leaderboard.Members {
|
||||||
lastMember := arrayFind(lastLeaderboard.Members, func(m memberData) bool { return m.ID == member.ID })
|
lastMember := arrayFind(lastLeaderboard.Members, func(m memberData) bool { return m.ID == member.ID })
|
||||||
if lastMember == nil {
|
if lastMember == nil {
|
||||||
// todo: report if they've already got stars on the year
|
// todo: report if they've already got stars on the year
|
||||||
nErr := sendNotification(fmt.Sprintf("A new challenger has appeared! Welcome, %s, to [the leaderboard](https://adventofcode.com/%s/leaderboard/private/view/%s)!", member.Name, *yearArg, leaderboardID))
|
nErr := sendNotification(fmt.Sprintf(":tada: A new challenger has appeared! Welcome, %s, to [the leaderboard](https://adventofcode.com/%s/leaderboard/private/view/%s)! :tada:", member.Name, *yearArg, leaderboardID))
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
log.Printf("Error sending new-challenger notification to the leaderboard for %s: %v\n", member.Name, nErr)
|
log.Printf("Error sending new-challenger notification to the leaderboard for %s: %v\n", member.Name, nErr)
|
||||||
}
|
}
|
||||||
@ -155,52 +167,56 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, day := range member.CompletionDayLevel {
|
for dayIdx, day := range member.CompletionDayLevel {
|
||||||
totalStars := getTotalStars(&member)
|
totalStars := getTotalStars(&member)
|
||||||
totalStarsPlural := "s"
|
totalStarsPlural := "s"
|
||||||
if totalStars == 1 {
|
if totalStars == 1 {
|
||||||
totalStarsPlural = ""
|
totalStarsPlural = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s := func(part *completionPartData, partNum int) {
|
||||||
|
completionTime := time.Unix(part.GotStarAt, 0).In(ChicagoTimeZone).Format("3:04:05pm")
|
||||||
|
rank := getCompletionRank(&leaderboard, &member, dayIdx, partNum) + 1
|
||||||
|
ordinal := getOrdinal(rank)
|
||||||
|
err := sendNotification(fmt.Sprintf(
|
||||||
|
":tada: %s completed day %d part %d %d%s on [the leaderboard](https://adventofcode.com/%s/leaderboard/private/view/%s) at %s! %s now has %d star%s on the year. :tada:",
|
||||||
|
member.Name,
|
||||||
|
dayIdx+1,
|
||||||
|
partNum,
|
||||||
|
rank,
|
||||||
|
ordinal,
|
||||||
|
*yearArg,
|
||||||
|
leaderboardID,
|
||||||
|
completionTime,
|
||||||
|
member.Name,
|
||||||
|
totalStars,
|
||||||
|
totalStarsPlural,
|
||||||
|
))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error sending notification for", member, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// todo: probably want to batch these for delivery later so we can sort by completion rank/time
|
// todo: probably want to batch these for delivery later so we can sort by completion rank/time
|
||||||
if day.Part1 != nil && lastMember.CompletionDayLevel[idx].Part1 == nil {
|
if day.Part1 != nil && lastMember.CompletionDayLevel[dayIdx].Part1 == nil {
|
||||||
completionTime := time.Unix(day.Part1.GetStarTimestamp, 0).In(ChicagoTimeZone).Format("3:04:05pm")
|
s(day.Part1, 1)
|
||||||
rank := getCompletionRank(&leaderboard, &member, idx, 1) + 1
|
|
||||||
ordinal := getOrdinal(rank)
|
|
||||||
sendNotification(fmt.Sprintf(
|
|
||||||
"%s completed day %d part 1 %d%s on [the leaderboard](https://adventofcode.com/%s/leaderboard/private/view/%s) at %s! %s now has %d star%s on the year.",
|
|
||||||
member.Name,
|
|
||||||
idx+1,
|
|
||||||
rank,
|
|
||||||
ordinal,
|
|
||||||
*yearArg,
|
|
||||||
leaderboardID,
|
|
||||||
completionTime,
|
|
||||||
member.Name,
|
|
||||||
totalStars,
|
|
||||||
totalStarsPlural,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
if day.Part2 != nil && lastMember.CompletionDayLevel[idx].Part2 == nil {
|
if day.Part2 != nil && lastMember.CompletionDayLevel[dayIdx].Part2 == nil {
|
||||||
completionTime := time.Unix(day.Part2.GetStarTimestamp, 0).In(ChicagoTimeZone).Format("3:04:05pm")
|
s(day.Part2, 2)
|
||||||
rank := getCompletionRank(&leaderboard, &member, idx, 2) + 1
|
|
||||||
ordinal := getOrdinal(rank)
|
|
||||||
sendNotification(fmt.Sprintf(
|
|
||||||
"%s completed day %d part 2 %d%s on [the leaderboard](https://adventofcode.com/%s/leaderboard/private/view/%s) at %s! %s now has %d star%s on the year.",
|
|
||||||
member.Name,
|
|
||||||
idx+1,
|
|
||||||
rank,
|
|
||||||
ordinal,
|
|
||||||
*yearArg,
|
|
||||||
leaderboardID,
|
|
||||||
completionTime,
|
|
||||||
member.Name,
|
|
||||||
totalStars,
|
|
||||||
totalStarsPlural,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c := cron.New()
|
||||||
|
c.AddFunc("*/15 * * * *", refresh)
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
quit := make(chan os.Signal, 2)
|
||||||
|
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
fmt.Println("Shutting down.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTotalStars(member *memberData) int {
|
func getTotalStars(member *memberData) int {
|
||||||
@ -218,9 +234,9 @@ func getTotalStars(member *memberData) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCompletionRank(leaderboard *leaderboardData, inMember *memberData, dayIdx int, partNum int) int {
|
func getCompletionRank(leaderboard *leaderboardData, inMember *memberData, dayIdx int, partNum int) int {
|
||||||
targetTime := inMember.CompletionDayLevel[dayIdx].Part1.GetStarTimestamp
|
targetTime := inMember.CompletionDayLevel[dayIdx].Part1.GotStarAt
|
||||||
if partNum != 1 {
|
if partNum != 1 {
|
||||||
targetTime = inMember.CompletionDayLevel[dayIdx].Part2.GetStarTimestamp
|
targetTime = inMember.CompletionDayLevel[dayIdx].Part2.GotStarAt
|
||||||
}
|
}
|
||||||
|
|
||||||
numAhead := 0
|
numAhead := 0
|
||||||
@ -237,7 +253,7 @@ func getCompletionRank(leaderboard *leaderboardData, inMember *memberData, dayId
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if part.GetStarTimestamp < targetTime {
|
if part.GotStarAt < targetTime {
|
||||||
numAhead++
|
numAhead++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,6 +350,9 @@ func sendNotification(content string) error {
|
|||||||
}{
|
}{
|
||||||
Text: content,
|
Text: content,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
fmt.Println("Sending notification:", content)
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Post(webhookURL.String(), "application/json", bytes.NewReader(b))
|
resp, err := http.DefaultClient.Post(webhookURL.String(), "application/json", bytes.NewReader(b))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error POSTing to webhook: %w", err)
|
return fmt.Errorf("error POSTing to webhook: %w", err)
|
||||||
|
Reference in New Issue
Block a user