Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,11 @@ Make sure to enable **MESSAGE CONTENT INTENT** for the bot:
LEADERBOARD_ID="<YOUR LEADERBOARD ID>"
DISCORD_TOKEN="<YOUR BOT's TOKEN>"
CHANNEL_ID="<THE CHANNEL YOU WANT THE BOT TO MONITOR>"
AOC_YEAR="<OPTIONAL: YEAR TO TRACK (defaults to current year)>"
```

**Note:** The `AOC_YEAR` variable is optional and defaults to the current year. You can set it to any year from 2015 onwards to track a specific Advent of Code event.

4. Build the project

```sh
Expand Down
8 changes: 6 additions & 2 deletions cmd/bot/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ func loadConfig() *config.Config {
if cfg == nil {
log.Fatal("cfg is nil")
}
if err := cfg.Validate(); err != nil {
log.Fatalf("configuration validation failed: %v", err)
}
log.Printf("Using Advent of Code year: %d", cfg.AOCYear)
return cfg
}

Expand All @@ -57,7 +61,7 @@ func getLeaderboard(cfg *config.Config) *aoc.Leaderboard {
if err != nil || file == nil {
log.Printf("error opening leaderboard file: %v", err)
log.Printf("getting leaderboard from AoC")
client := aoc.NewClient(cfg.SessionCookie)
client := aoc.NewClient(cfg.SessionCookie, cfg.AOCYear)
storedLeaderboard, err := client.GetLeaderboard(cfg.LeaderboardID)
return handleLeaderboardError(storedLeaderboard, err)
}
Expand All @@ -73,7 +77,7 @@ func handleLeaderboardError(leaderboard *aoc.Leaderboard, err error) *aoc.Leader
}

func initTracker(cfg *config.Config, storedLeaderboard *aoc.Leaderboard) *leaderboard.Tracker {
client := aoc.NewClient(cfg.SessionCookie)
client := aoc.NewClient(cfg.SessionCookie, cfg.AOCYear)
tracker := leaderboard.NewTracker(cfg, storedLeaderboard, client)
if tracker == nil {
log.Fatal("tracker is nil")
Expand Down
8 changes: 5 additions & 3 deletions internal/aoc/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
type Client struct {
SessionCookie string
HTTPClient *http.Client
Year int
}

// NewClient creates a new AOC client with the provided session cookie.
func NewClient(sessionCookie string) *Client {
// NewClient creates a new AOC client with the provided session cookie and year.
func NewClient(sessionCookie string, year int) *Client {
return &Client{
SessionCookie: sessionCookie,
HTTPClient: http.DefaultClient,
Year: year,
}
}

Expand All @@ -26,7 +28,7 @@ func (c *Client) SetHTTPClient(client *http.Client) {
}

func (c *Client) GetLeaderboard(leaderboardID string) (*Leaderboard, error) {
url := fmt.Sprintf("https://adventofcode.com/2024/leaderboard/private/view/%s.json", leaderboardID)
url := fmt.Sprintf("https://adventofcode.com/%d/leaderboard/private/view/%s.json", c.Year, leaderboardID)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/aoc/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestGetLeaderboardSuccess(t *testing.T) {
}))
defer mockServer.Close()

client := NewClient("test-session-cookie")
client := NewClient("test-session-cookie", 2024)
// Inject mock HTTP client
client.SetHTTPClient(mockServer.Client())

Expand Down Expand Up @@ -110,7 +110,7 @@ func TestGetLeaderboardInvalidSession(t *testing.T) {
}))
defer mockServer.Close()

client := NewClient("invalid-session-cookie")
client := NewClient("invalid-session-cookie", 2024)
client.SetHTTPClient(mockServer.Client())

// Override the request URL
Expand All @@ -133,7 +133,7 @@ func TestGetLeaderboardInvalidJSON(t *testing.T) {
}))
defer mockServer.Close()

client := NewClient("test-session-cookie")
client := NewClient("test-session-cookie", 2024)
client.SetHTTPClient(mockServer.Client())

// Override the request URL
Expand Down
33 changes: 33 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
package config

import (
"fmt"
"os"
"strconv"
"time"
)

type Config struct {
LeaderboardID string
SessionCookie string
DiscordToken string
ChannelID string
AOCYear int
}

func NewConfig() *Config {
// Default to current year if AOC_YEAR is not set
year := time.Now().Year()
if yearStr := os.Getenv("AOC_YEAR"); yearStr != "" {
if parsedYear, err := strconv.Atoi(yearStr); err == nil && parsedYear > 2015 {
year = parsedYear
}
}

return &Config{
LeaderboardID: os.Getenv("LEADERBOARD_ID"),
SessionCookie: os.Getenv("SESSION_COOKIE"),
DiscordToken: os.Getenv("DISCORD_TOKEN"),
ChannelID: os.Getenv("CHANNEL_ID"),
AOCYear: year,
}
}

// Validate checks that all required configuration values are present.
func (c *Config) Validate() error {
if c.LeaderboardID == "" {
return fmt.Errorf("LEADERBOARD_ID environment variable is required")
}
if c.SessionCookie == "" {
return fmt.Errorf("SESSION_COOKIE environment variable is required")
}
if c.DiscordToken == "" {
return fmt.Errorf("DISCORD_TOKEN environment variable is required")
}
if c.ChannelID == "" {
return fmt.Errorf("CHANNEL_ID environment variable is required")
}
if c.AOCYear < 2015 {
return fmt.Errorf("AOC_YEAR must be 2015 or later (Advent of Code started in 2015)")
}
return nil
}
121 changes: 121 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand All @@ -13,6 +14,7 @@ func TestNewConfig(t *testing.T) {
t.Setenv("SESSION_COOKIE", "prod-session-cookie")
t.Setenv("DISCORD_TOKEN", "prod-discord-token")
t.Setenv("CHANNEL_ID", "prod-channel-id")
t.Setenv("AOC_YEAR", "2023")

// Call NewConfig
cfg := NewConfig()
Expand All @@ -22,6 +24,7 @@ func TestNewConfig(t *testing.T) {
assert.Equal(t, "prod-session-cookie", cfg.SessionCookie, "SessionCookie should match")
assert.Equal(t, "prod-discord-token", cfg.DiscordToken, "DiscordToken should match")
assert.Equal(t, "prod-channel-id", cfg.ChannelID, "ChannelID should match")
assert.Equal(t, 2023, cfg.AOCYear, "AOCYear should match")
})

t.Run("Some Environment Variables Missing", func(t *testing.T) {
Expand All @@ -38,6 +41,7 @@ func TestNewConfig(t *testing.T) {
assert.Equal(t, "prod-session-cookie", cfg.SessionCookie, "SessionCookie should match")
assert.Equal(t, "", cfg.DiscordToken, "DiscordToken should be empty")
assert.Equal(t, "", cfg.ChannelID, "ChannelID should be empty")
assert.Equal(t, time.Now().Year(), cfg.AOCYear, "AOCYear should default to current year")
})

t.Run("No Environment Variables Set", func(t *testing.T) {
Expand All @@ -51,6 +55,40 @@ func TestNewConfig(t *testing.T) {
assert.Equal(t, "", cfg.SessionCookie, "SessionCookie should be empty")
assert.Equal(t, "", cfg.DiscordToken, "DiscordToken should be empty")
assert.Equal(t, "", cfg.ChannelID, "ChannelID should be empty")
assert.Equal(t, time.Now().Year(), cfg.AOCYear, "AOCYear should default to current year")
})

t.Run("Custom AOC Year Set", func(t *testing.T) {
// Set custom year
t.Setenv("AOC_YEAR", "2022")

// Call NewConfig
cfg := NewConfig()

// Assertions
assert.Equal(t, 2022, cfg.AOCYear, "AOCYear should be 2022")
})

t.Run("Invalid AOC Year Defaults to Current Year", func(t *testing.T) {
// Set invalid year
t.Setenv("AOC_YEAR", "not-a-number")

// Call NewConfig
cfg := NewConfig()

// Assertions
assert.Equal(t, time.Now().Year(), cfg.AOCYear, "AOCYear should default to current year when invalid")
})

t.Run("AOC Year Below 2015 Defaults to Current Year", func(t *testing.T) {
// Set year before Advent of Code existed
t.Setenv("AOC_YEAR", "2014")

// Call NewConfig
cfg := NewConfig()

// Assertions
assert.Equal(t, time.Now().Year(), cfg.AOCYear, "AOCYear should default to current year when below 2015")
})
}

Expand All @@ -61,6 +99,7 @@ func TestConfigStruct(t *testing.T) {
t.Setenv("SESSION_COOKIE", "config-session-cookie")
t.Setenv("DISCORD_TOKEN", "config-discord-token")
t.Setenv("CHANNEL_ID", "config-channel-id")
t.Setenv("AOC_YEAR", "2024")

// Initialize Config
cfg := NewConfig()
Expand All @@ -71,8 +110,90 @@ func TestConfigStruct(t *testing.T) {
SessionCookie: "config-session-cookie",
DiscordToken: "config-discord-token",
ChannelID: "config-channel-id",
AOCYear: 2024,
}

assert.Equal(t, expected, cfg, "Config struct should match expected values")
})
}

func TestValidate(t *testing.T) {
t.Run("Valid Config", func(t *testing.T) {
cfg := &Config{
LeaderboardID: "test-leaderboard",
SessionCookie: "test-cookie",
DiscordToken: "test-token",
ChannelID: "test-channel",
AOCYear: 2024,
}

err := cfg.Validate()
assert.NoError(t, err, "Valid config should not return an error")
})

t.Run("Missing LeaderboardID", func(t *testing.T) {
cfg := &Config{
SessionCookie: "test-cookie",
DiscordToken: "test-token",
ChannelID: "test-channel",
AOCYear: 2024,
}

err := cfg.Validate()
assert.Error(t, err, "Should return error for missing LeaderboardID")
assert.Contains(t, err.Error(), "LEADERBOARD_ID", "Error should mention LEADERBOARD_ID")
})

t.Run("Missing SessionCookie", func(t *testing.T) {
cfg := &Config{
LeaderboardID: "test-leaderboard",
DiscordToken: "test-token",
ChannelID: "test-channel",
AOCYear: 2024,
}

err := cfg.Validate()
assert.Error(t, err, "Should return error for missing SessionCookie")
assert.Contains(t, err.Error(), "SESSION_COOKIE", "Error should mention SESSION_COOKIE")
})

t.Run("Missing DiscordToken", func(t *testing.T) {
cfg := &Config{
LeaderboardID: "test-leaderboard",
SessionCookie: "test-cookie",
ChannelID: "test-channel",
AOCYear: 2024,
}

err := cfg.Validate()
assert.Error(t, err, "Should return error for missing DiscordToken")
assert.Contains(t, err.Error(), "DISCORD_TOKEN", "Error should mention DISCORD_TOKEN")
})

t.Run("Missing ChannelID", func(t *testing.T) {
cfg := &Config{
LeaderboardID: "test-leaderboard",
SessionCookie: "test-cookie",
DiscordToken: "test-token",
AOCYear: 2024,
}

err := cfg.Validate()
assert.Error(t, err, "Should return error for missing ChannelID")
assert.Contains(t, err.Error(), "CHANNEL_ID", "Error should mention CHANNEL_ID")
})

t.Run("AOCYear Below 2015", func(t *testing.T) {
cfg := &Config{
LeaderboardID: "test-leaderboard",
SessionCookie: "test-cookie",
DiscordToken: "test-token",
ChannelID: "test-channel",
AOCYear: 2014,
}

err := cfg.Validate()
assert.Error(t, err, "Should return error for AOCYear below 2015")
assert.Contains(t, err.Error(), "AOC_YEAR", "Error should mention AOC_YEAR")
})
}