Skip to content

Commit 0e813de

Browse files
committed
catch session expirations and fail gracefully
refactor guild configuration to support future features start building snapshots for future "Christmas in July" event
1 parent 2962dd4 commit 0e813de

File tree

6 files changed

+127
-44
lines changed

6 files changed

+127
-44
lines changed

Diff for: advent-of-code.go

+22-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"fmt"
45
"log"
56
"net/http"
67
"net/url"
@@ -15,39 +16,42 @@ const userAgent = "github.com/Alextopher/aocbot"
1516
type AdventOfCode struct {
1617
sync.RWMutex
1718

18-
year string
1919
id string
2020
sessionCookie string
2121

22-
leaderboard *Leaderboard
23-
lastUpdated time.Time
22+
leaderboards map[string]*Leaderboard
23+
lastUpdated time.Time
2424
}
2525

2626
// NewAdventOfCode creates a new Advent of Code API
27-
func NewAdventOfCode(sessionCookie string, year string, id string) *AdventOfCode {
27+
func NewAdventOfCode(sessionCookie string, id string) *AdventOfCode {
2828
return &AdventOfCode{
2929
sessionCookie: sessionCookie,
30-
year: year,
30+
leaderboards: make(map[string]*Leaderboard),
3131
id: id,
3232
}
3333
}
3434

3535
// GetLeaderboard gets the most recent leaderboard data from the API
36-
func (aoc *AdventOfCode) GetLeaderboard() *Leaderboard {
36+
func (aoc *AdventOfCode) GetLeaderboard(year string) *Leaderboard {
3737
if time.Since(aoc.lastUpdated) > 15*time.Minute {
38-
aoc.UpdateLeaderboard()
38+
aoc.UpdateLeaderboard(year)
3939
}
4040

4141
aoc.RLock()
42-
leaderboard := aoc.leaderboard
42+
leaderboard, ok := aoc.leaderboards[year]
43+
if !ok {
44+
log.Println("Leaderboard not found for year: ", year)
45+
}
4346
aoc.RUnlock()
4447

4548
return leaderboard
4649
}
4750

4851
// UpdateLeaderboard updates a leaderboard by getting the latest data from the API
49-
func (aoc *AdventOfCode) UpdateLeaderboard() error {
50-
requestURL := "https://adventofcode.com/" + aoc.year + "/leaderboard/private/view/" + aoc.id + ".json"
52+
func (aoc *AdventOfCode) UpdateLeaderboard(year string) error {
53+
requestURL := "https://adventofcode.com/" + year + "/leaderboard/private/view/" + aoc.id + ".json"
54+
fmt.Println(requestURL)
5155

5256
url, err := url.Parse(requestURL)
5357
if err != nil {
@@ -70,17 +74,23 @@ func (aoc *AdventOfCode) UpdateLeaderboard() error {
7074
return err
7175
}
7276

77+
// Check the content type of the response, if it's not JSON then we can't parse it
78+
contentType := response.Header.Get("Content-Type")
79+
if contentType != "application/json" {
80+
return ErrInvalidSession
81+
}
82+
7383
leaderboard, err := ParseLeaderboard(response.Body)
7484
if err != nil {
7585
log.Println("Error while parsing response: ", err)
7686
return err
7787
}
7888

7989
aoc.Lock()
80-
aoc.leaderboard = leaderboard
90+
aoc.leaderboards[year] = leaderboard
8191
aoc.lastUpdated = time.Now()
8292
aoc.Unlock()
8393

84-
log.Println("Updated leaderboard for (" + aoc.year + ", " + aoc.id + ")")
94+
log.Printf("Updated leaderboard for (%s, %s)\n", aoc.id, year)
8595
return nil
8696
}

Diff for: bot.go

+27-15
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"log"
66
"os"
7+
"time"
78

89
"github.com/bwmarrin/discordgo"
910
)
@@ -30,19 +31,19 @@ func NewBot(session *discordgo.Session, sessionCookie string) *Bot {
3031
}
3132

3233
// AddGuild adds a guild to the bot
33-
func (bot *Bot) AddGuild(guildID string, year string, leaderboardID string) (err error) {
34+
func (bot *Bot) AddGuild(guildID string, guildConfig GuildConfig) (err error) {
3435
// Create guild logFile file
3536
logFile, err := os.OpenFile(fmt.Sprintf("logs/%s.db", guildID), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
3637
if err != nil {
3738
return err
3839
}
3940

40-
bot.states[guildID], err = NewGuildState(bot.sessionCookie, year, leaderboardID, logFile)
41+
bot.states[guildID], err = NewGuildState(bot.sessionCookie, guildConfig, logFile)
4142
if err != nil {
4243
return err
4344
}
4445

45-
return bot.states[guildID].adventOfCode.UpdateLeaderboard()
46+
return bot.states[guildID].adventOfCode.UpdateLeaderboard(guildConfig.Year)
4647
}
4748

4849
// Start starts the bot (and waits for it to be ready)
@@ -80,6 +81,12 @@ func (bot *Bot) Sync() {
8081
func (bot *Bot) CreateRoles(guild *discordgo.Guild) error {
8182
True := true
8283

84+
// Get the guild state
85+
guildState, ok := bot.states[guild.ID]
86+
if !ok {
87+
return ErrNotConfigured
88+
}
89+
8390
// Spoiler role (allows users to access all channels)
8491
if !bot.CheckRole(guild, "Spoiler") {
8592
_, err := bot.session.GuildRoleCreate(guild.ID, &discordgo.RoleParams{
@@ -117,16 +124,18 @@ func (bot *Bot) CreateRoles(guild *discordgo.Guild) error {
117124
}
118125
}
119126

120-
for i := 25; i > 0; i-- {
121-
name := fmt.Sprintf("Day %02d", i)
127+
if guildState.daily_roles {
128+
for i := 25; i > 0; i-- {
129+
name := fmt.Sprintf("Day %02d", i)
122130

123-
if !bot.CheckRole(guild, name) {
124-
_, err := bot.session.GuildRoleCreate(guild.ID, &discordgo.RoleParams{
125-
Name: name,
126-
})
131+
if !bot.CheckRole(guild, name) {
132+
_, err := bot.session.GuildRoleCreate(guild.ID, &discordgo.RoleParams{
133+
Name: name,
134+
})
127135

128-
if err != nil {
129-
return err
136+
if err != nil {
137+
return err
138+
}
130139
}
131140
}
132141
}
@@ -203,7 +212,7 @@ func (bot *Bot) SyncMemberRoles(guild *discordgo.Guild, guildMember *discordgo.M
203212
return ErrDoesNotExist
204213
}
205214

206-
return bot.syncRoles(guild, guildMember, member)
215+
return bot.syncRoles(guild, guildMember, member, guildState.daily_roles)
207216
}
208217

209218
// SyncAllRoles updates each user's roles to reflect their current star count.
@@ -216,6 +225,7 @@ func (bot *Bot) SyncAllRoles(guild *discordgo.Guild) error {
216225
// Get the leaderboard
217226
leaderboard := guildState.GetLeaderboard()
218227

228+
log.Printf("Syncing roles for %s %t\n", guild.Name, guildState.daily_roles)
219229
guildState.db.GoForEach(func(discord_id, advent_id string) {
220230
member, ok := leaderboard.GetMemberByID(advent_id)
221231
if !ok {
@@ -230,7 +240,7 @@ func (bot *Bot) SyncAllRoles(guild *discordgo.Guild) error {
230240
return
231241
}
232242

233-
err = bot.syncRoles(guild, guildMember, member)
243+
err = bot.syncRoles(guild, guildMember, member, guildState.daily_roles)
234244
if err != nil {
235245
return
236246
}
@@ -240,7 +250,7 @@ func (bot *Bot) SyncAllRoles(guild *discordgo.Guild) error {
240250
}
241251

242252
// syncRoles reduces code duplication between SyncRoles and SyncAllRoles
243-
func (bot *Bot) syncRoles(guild *discordgo.Guild, guildMember *discordgo.Member, member *Member) error {
253+
func (bot *Bot) syncRoles(guild *discordgo.Guild, guildMember *discordgo.Member, member *Member, daily_roles bool) error {
244254
stars := member.Stars
245255

246256
// 10, 20, 30, 40, 50 stars
@@ -251,6 +261,7 @@ func (bot *Bot) syncRoles(guild *discordgo.Guild, guildMember *discordgo.Member,
251261
log.Println("Error (syncRoles) adding/removing role: ", err)
252262
return err
253263
}
264+
time.Sleep(1 * time.Second)
254265
}
255266

256267
// Connected
@@ -264,11 +275,12 @@ func (bot *Bot) syncRoles(guild *discordgo.Guild, guildMember *discordgo.Member,
264275
for day := 1; day <= 25; day++ {
265276
role := fmt.Sprintf("Day %02d", day)
266277
shouldAdd := member.CompletionDayLevel[day] != nil && len(member.CompletionDayLevel[day]) > 0
267-
err := bot.AddOrRemoveRole(guild, guildMember, role, shouldAdd)
278+
err := bot.AddOrRemoveRole(guild, guildMember, role, shouldAdd && daily_roles)
268279
if err != nil {
269280
log.Println("Error (syncRoles) adding/removing role: ", err)
270281
return err
271282
}
283+
time.Sleep(1 * time.Second)
272284
}
273285

274286
return nil

Diff for: config.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import (
55
"io"
66
)
77

8+
// GuildConfig is the config per guild
9+
type GuildConfig struct {
10+
Year string `json:"year"`
11+
Mode string `json:"mode"`
12+
LeaderboardID string `json:"leaderboard_id"`
13+
DailyRoles bool `json:"daily_roles"`
14+
}
15+
816
// Config is the bot config
917
type Config struct {
1018
// The Discord bot token
@@ -14,11 +22,7 @@ type Config struct {
1422
SessionCookie string `json:"session_cookie"`
1523

1624
// Map guild ids to (year, leaderboard id) pairs
17-
Guilds map[string]struct {
18-
Year string `json:"year"`
19-
LeaderboardID string `json:"leaderboard_id"`
20-
DailyRoles bool `json:"daily_roles"`
21-
} `json:"guilds"`
25+
Guilds map[string]GuildConfig `json:"guilds"`
2226
}
2327

2428
// ParseConfig parses a config file

Diff for: db.go

+56-6
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import (
88

99
// DatabaseEvent is a single database event
1010
type DatabaseEvent struct {
11-
Create *EventCreate `json:"create,omitempty"`
12-
Delete *EventDelete `json:"delete,omitempty"`
11+
Create *EventCreate `json:"create,omitempty"`
12+
Delete *EventDelete `json:"delete,omitempty"`
13+
Snapshot *EventSnapshot `json:"snapshot,omitempty"`
1314
}
1415

1516
// EventCreate is a database event for creating a claim
@@ -42,18 +43,38 @@ func NewEventDelete(discordID string) *DatabaseEvent {
4243
}
4344
}
4445

45-
// Keeps an append-only log file of user claims
46-
// (basically keeps a persistent map[string]string)
46+
// EventSnapshot is a database event for taking a snapshot of the total scores
47+
type EventSnapshot struct {
48+
Timestamp int64 `json:"timestamp"`
49+
Scores map[string]int `json:"scores"`
50+
}
51+
52+
// NewEventSnapshot creates a new database event for taking a snapshot of the total scores
53+
func NewEventSnapshot(timestamp int64, scores map[string]int) *DatabaseEvent {
54+
return &DatabaseEvent{
55+
Snapshot: &EventSnapshot{
56+
Timestamp: timestamp,
57+
Scores: scores,
58+
},
59+
}
60+
}
4761

48-
// Database keeps an append-only log file of user name claims and unclaims
62+
// Database keeps an append-only log file of bot operation
63+
//
64+
// - tracks APOD id claims
65+
// - tracks APOD id unclaims
66+
// - creates APOD total score snapshots
4967
type Database struct {
5068
sync.RWMutex
5169

52-
// Where to write new events to
70+
// Where we write new events to
5371
writer *json.Encoder
5472

5573
// In-memory, per guild, mapping of discord ids to Advent of Code ids
5674
mappings map[string]string
75+
76+
// Timestamped snapshot of total scores
77+
scores map[string]int
5778
}
5879

5980
// NewDatabase creates a new database
@@ -62,6 +83,7 @@ func NewDatabase(reader io.Reader, writer io.Writer) (*Database, error) {
6283
database := &Database{
6384
writer: json.NewEncoder(writer),
6485
mappings: make(map[string]string),
86+
scores: make(map[string]int),
6587
}
6688

6789
decoder := json.NewDecoder(reader)
@@ -165,6 +187,34 @@ func (database *Database) CheckClaim(adventID string) bool {
165187
return false
166188
}
167189

190+
// Snapshot takes a snapshot of the total scores
191+
func (database *Database) Snapshot(timestamp int64, scores map[string]int) error {
192+
database.Lock()
193+
194+
// Take a snapshot of the total scores
195+
database.scores = scores
196+
197+
// Write the event to the database
198+
err := database.writer.Encode(NewEventSnapshot(timestamp, scores))
199+
200+
database.Unlock()
201+
return err
202+
}
203+
204+
// GetScores gets the change in total scores since the last snapshot
205+
func (database *Database) GetScores(currentScores map[string]int) map[string]int {
206+
database.RLock()
207+
208+
// Get the change in total scores
209+
scores := make(map[string]int)
210+
for id, score := range currentScores {
211+
scores[id] = score - database.scores[id]
212+
}
213+
214+
database.RUnlock()
215+
return scores
216+
}
217+
168218
// GoForEach iterates over each claim and calls a function
169219
func (database *Database) GoForEach(fn func(discord_id, advent_id string)) {
170220
database.RLock()

Diff for: main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func main() {
3939

4040
// Add guilds
4141
for guildID, guildConfig := range config.Guilds {
42-
err = bot.AddGuild(guildID, guildConfig.Year, guildConfig.LeaderboardID)
42+
err = bot.AddGuild(guildID, guildConfig)
4343
if err != nil {
4444
log.Fatalln("Error adding guild: ", err)
4545
}

Diff for: state.go

+12-5
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,29 @@ var ErrAlreadyClaimed = errors.New("user is already claimed")
1515
// ErrDoesNotExist is returned when a user does not exist
1616
var ErrDoesNotExist = errors.New("user does not exist")
1717

18+
// ErrInvalidSession is returned when the advent of code session is invalid
19+
var ErrInvalidSession = errors.New("advent of code session has expired, please update the session cookie")
20+
1821
// GuildState keeps track of the state of a single guild
1922
type GuildState struct {
2023
adventOfCode *AdventOfCode
2124
db *Database
25+
year string
26+
daily_roles bool
2227
}
2328

2429
// NewGuildState creates a new guild state
25-
func NewGuildState(sessionCookie, year, id string, log *os.File) (*GuildState, error) {
30+
func NewGuildState(sessionCookie string, config GuildConfig, log *os.File) (*GuildState, error) {
2631
database, err := NewDatabase(log, log)
2732
if err != nil {
2833
return nil, err
2934
}
3035

3136
return &GuildState{
32-
adventOfCode: NewAdventOfCode(sessionCookie, year, id),
37+
adventOfCode: NewAdventOfCode(sessionCookie, config.LeaderboardID),
3338
db: database,
39+
year: config.Year,
40+
daily_roles: config.DailyRoles,
3441
}, nil
3542
}
3643

@@ -90,11 +97,11 @@ func (guildState *GuildState) CloseNames(username string) ([]string, error) {
9097

9198
// GetLeaderboard is wrapper for guildState.adventOfCode.GetLeaderboard()
9299
func (guildState *GuildState) GetLeaderboard() *Leaderboard {
93-
return guildState.adventOfCode.GetLeaderboard()
100+
return guildState.adventOfCode.GetLeaderboard(guildState.year)
94101
}
95102

96103
// UpdateLeaderboard updates the leaderboard before returning it
97104
func (guildState *GuildState) UpdateLeaderboard() *Leaderboard {
98-
guildState.adventOfCode.UpdateLeaderboard()
99-
return guildState.adventOfCode.GetLeaderboard()
105+
guildState.adventOfCode.UpdateLeaderboard(guildState.year)
106+
return guildState.adventOfCode.GetLeaderboard(guildState.year)
100107
}

0 commit comments

Comments
 (0)