From 95e760c549e2e4a20a64f1fae11930b5d0701735 Mon Sep 17 00:00:00 2001 From: Xevion Date: Tue, 26 Aug 2025 11:53:29 -0500 Subject: [PATCH] feat: move default term acquisition & season range from global into config state, LoadLocation once, remove bare constants --- internal/api/api.go | 11 +++- internal/api/scrape.go | 7 +-- internal/bot/state.go | 2 +- internal/config/config.go | 9 ++-- internal/{api/term.go => config/terms.go} | 61 +++++++++++------------ 5 files changed, 48 insertions(+), 42 deletions(-) rename internal/{api/term.go => config/terms.go} (72%) diff --git a/internal/api/api.go b/internal/api/api.go index 30a9cb0..a7e778b 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -37,7 +37,7 @@ var ( ) // SessionMiddleware creates a Resty middleware that resets the session timer on each successful Banner API call. -func SessionMiddleware(c *resty.Client, r *resty.Response) error { +func SessionMiddleware(_ *resty.Client, r *resty.Response) error { // log.Debug().Str("url", r.Request.RawRequest.URL.Path).Msg("Session middleware") // Reset session timer on successful requests to Banner API endpoints @@ -56,6 +56,15 @@ func GenerateSession() string { return internal.RandomString(5) + internal.Nonce() } +// DefaultTerm returns the default term, which is the current term if it exists, otherwise the next term. +func (a *API) DefaultTerm(t time.Time) config.Term { + currentTerm, nextTerm := config.GetCurrentTerm(*a.config.SeasonRanges, t) + if currentTerm == nil { + return *nextTerm + } + return *currentTerm +} + var terms []BannerTerm var lastTermUpdate time.Time diff --git a/internal/api/scrape.go b/internal/api/scrape.go index 78b01dc..7d54320 100644 --- a/internal/api/scrape.go +++ b/internal/api/scrape.go @@ -14,6 +14,7 @@ import ( ) const ( + // MaxPageSize is the maximum number of courses one can scrape per page. MaxPageSize = 500 ) @@ -75,7 +76,7 @@ func (a *API) Scrape() error { // GetExpiredSubjects returns a list of subjects that have expired and should be scraped again. // It checks Redis for the "scraped" status of each major for the current term. func (a *API) GetExpiredSubjects() ([]string, error) { - term := Default(time.Now()).ToString() + term := a.DefaultTerm(time.Now()).ToString() subjects := make([]string, 0) // Create a timeout context for Redis operations @@ -114,7 +115,7 @@ func (a *API) ScrapeMajor(subject string) error { for { // Build & execute the query query := NewQuery().Offset(offset).MaxResults(MaxPageSize * 2).Subject(subject) - term := Default(time.Now()).ToString() + term := a.DefaultTerm(time.Now()).ToString() result, err := a.Search(term, query, "subjectDescription", false) if err != nil { return fmt.Errorf("search failed: %w (%s)", err, query.String()) @@ -157,7 +158,7 @@ func (a *API) ScrapeMajor(subject string) error { break } - term := Default(time.Now()).ToString() + term := a.DefaultTerm(time.Now()).ToString() // Calculate the expiry time for the scrape (1 hour for every 200 classes, random +-15%) with a minimum of 1 hour var scrapeExpiry time.Duration diff --git a/internal/bot/state.go b/internal/bot/state.go index 27a309b..d04c6d5 100644 --- a/internal/bot/state.go +++ b/internal/bot/state.go @@ -32,7 +32,7 @@ func (b *Bot) SetClosing() { // GetSession ensures a valid session is available and selects the default term. func (b *Bot) GetSession() (string, error) { sessionID := b.API.EnsureSession() - term := api.Default(time.Now()).ToString() + term := b.API.DefaultTerm(time.Now()).ToString() log.Info().Str("term", term).Str("sessionID", sessionID).Msg("Setting selected term") err := b.API.SelectTerm(term, sessionID) diff --git a/internal/config/config.go b/internal/config/config.go index 91f03f6..1d47a3a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -26,12 +26,10 @@ type Config struct { Environment string // CentralTimeLocation is the time.Location for US Central Time. CentralTimeLocation *time.Location + // SeasonRanges is the time.Location for US Central Time. + SeasonRanges *SeasonRanges } -const ( - CentralTimezoneName = "America/Chicago" -) - // New creates a new Config instance with a cancellable context. func New() (*Config, error) { ctx, cancel := context.WithCancel(context.Background()) @@ -42,10 +40,13 @@ func New() (*Config, error) { return nil, err } + seasonRanges := GetYearDayRange(loc, uint16(time.Now().Year())) + return &Config{ Ctx: ctx, CancelFunc: cancel, CentralTimeLocation: loc, + SeasonRanges: &seasonRanges, }, nil } diff --git a/internal/api/term.go b/internal/config/terms.go similarity index 72% rename from internal/api/term.go rename to internal/config/terms.go index 174d460..d603b5e 100644 --- a/internal/api/term.go +++ b/internal/config/terms.go @@ -1,7 +1,6 @@ -package api +package config import ( - "banner/internal/config" "fmt" "strconv" "time" @@ -13,9 +12,12 @@ import ( // Summer 2024, "fall" => Fall 2024 const ( - Spring = iota + // Fall is the first term of the school year. + Fall = iota + // Spring is the second term of the school year. + Spring + // Summer is the third term of the school year. Summer - Fall ) // Term represents a school term, consisting of a year and a season. @@ -24,13 +26,11 @@ type Term struct { Season uint8 } -var ( - SpringRange, SummerRange, FallRange YearDayRange -) - -func init() { - loc, _ := time.LoadLocation(config.CentralTimezoneName) - SpringRange, SummerRange, FallRange = GetYearDayRange(loc, uint16(time.Now().Year())) +// SeasonRanges represents the start and end day of each term within a year. +type SeasonRanges struct { + Spring YearDayRange + Summer YearDayRange + Fall YearDayRange } // YearDayRange represents the start and end day of a term within a year. @@ -41,7 +41,7 @@ type YearDayRange struct { // GetYearDayRange returns the start and end day of each term for the given year. // The ranges are inclusive of the start day and exclusive of the end day. -func GetYearDayRange(loc *time.Location, year uint16) (YearDayRange, YearDayRange, YearDayRange) { +func GetYearDayRange(loc *time.Location, year uint16) SeasonRanges { springStart := time.Date(int(year), time.January, 14, 0, 0, 0, 0, loc).YearDay() springEnd := time.Date(int(year), time.May, 1, 0, 0, 0, 0, loc).YearDay() summerStart := time.Date(int(year), time.May, 25, 0, 0, 0, 0, loc).YearDay() @@ -49,49 +49,53 @@ func GetYearDayRange(loc *time.Location, year uint16) (YearDayRange, YearDayRang fallStart := time.Date(int(year), time.August, 18, 0, 0, 0, 0, loc).YearDay() fallEnd := time.Date(int(year), time.December, 10, 0, 0, 0, 0, loc).YearDay() - return YearDayRange{ + return SeasonRanges{ + Spring: YearDayRange{ Start: uint16(springStart), End: uint16(springEnd), - }, YearDayRange{ + }, + Summer: YearDayRange{ Start: uint16(summerStart), End: uint16(summerEnd), - }, YearDayRange{ + }, + Fall: YearDayRange{ Start: uint16(fallStart), End: uint16(fallEnd), - } + }, + } } // GetCurrentTerm returns the current and next terms based on the provided time. // The current term can be nil if the time falls between terms. // The 'year' in the term corresponds to the academic year, which may differ from the calendar year. -func GetCurrentTerm(now time.Time) (*Term, *Term) { +func GetCurrentTerm(ranges SeasonRanges, now time.Time) (*Term, *Term) { literalYear := uint16(now.Year()) dayOfYear := uint16(now.YearDay()) // If we're past the end of the summer term, we're 'in' the next school year. var termYear uint16 - if dayOfYear > SummerRange.End { + if dayOfYear > ranges.Summer.End { termYear = literalYear + 1 } else { termYear = literalYear } - if (dayOfYear < SpringRange.Start) || (dayOfYear >= FallRange.End) { + if (dayOfYear < ranges.Spring.Start) || (dayOfYear >= ranges.Fall.End) { // Fall over, Spring not yet begun return nil, &Term{Year: termYear, Season: Spring} - } else if (dayOfYear >= SpringRange.Start) && (dayOfYear < SpringRange.End) { + } else if (dayOfYear >= ranges.Spring.Start) && (dayOfYear < ranges.Spring.End) { // Spring return &Term{Year: termYear, Season: Spring}, &Term{Year: termYear, Season: Summer} - } else if dayOfYear < SummerRange.Start { + } else if dayOfYear < ranges.Summer.Start { // Spring over, Summer not yet begun return nil, &Term{Year: termYear, Season: Summer} - } else if (dayOfYear >= SummerRange.Start) && (dayOfYear < SummerRange.End) { + } else if (dayOfYear >= ranges.Summer.Start) && (dayOfYear < ranges.Summer.End) { // Summer return &Term{Year: termYear, Season: Summer}, &Term{Year: termYear, Season: Fall} - } else if dayOfYear < FallRange.Start { + } else if dayOfYear < ranges.Fall.Start { // Summer over, Fall not yet begun return nil, &Term{Year: termYear, Season: Fall} - } else if (dayOfYear >= FallRange.Start) && (dayOfYear < FallRange.End) { + } else if (dayOfYear >= ranges.Fall.Start) && (dayOfYear < ranges.Fall.End) { // Fall return &Term{Year: termYear, Season: Fall}, nil } @@ -134,12 +138,3 @@ func (term Term) ToString() string { return fmt.Sprintf("%d%s", term.Year, season) } - -// Default returns the default term, which is the current term if it exists, otherwise the next term. -func Default(t time.Time) Term { - currentTerm, nextTerm := GetCurrentTerm(t) - if currentTerm == nil { - return *nextTerm - } - return *currentTerm -}